Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CuttingBoardSyndicate

Git Source

Inherits: Upgradeable

Title: CuttingBoardSyndicate

Enables multiple partners to collectively win a cutting board Dutch auction through a priority-based bidding system. Partners bid on weight slices of the 10 000 bps validator board; fill order is determined by bid value (weight × maxPricePerBps), and the current Dutch auction price gates eligibility per partner.

The syndicate owns the minted CuttingBoardNFT after a successful claim and submits proposals to CuttingBoardManager on behalf of partners. Each round is identified by its auctionId. Rounds for different auctions (including concurrent auctions for different validators, or a new auction that opens before the previous allocation period has expired) are fully independent. There is no global state shared between rounds. External parties are welcome to bid directly on the underlying Dutch auction. Rounds that fail to trigger before the auction window closes are expected and handled cleanly via expireRound(). Round lifecycle ─────────────── (new auctionId) │ Idle ──openRound()──► Open ──triggerClaim()──► Active (terminal) │ (conditions met) NFT held until allocation expires expireRound() (auction lapsed) │ Expired (terminal; all deposits refunded) Bidding model ───────────── Each partner registers (vault, weight, maxPricePerBps). weight must be a positive multiple of minSlotWeight and at most maxWeightPerVault. A deposit of maxPricePerBps × weight is collected upfront. At trigger time:

  1. Eligible partners (maxPricePerBps × 10 000 ≥ currentPrice) are sorted by bid value (weight × maxPricePerBps) descending; ties break on registration order.
  2. Weight is allocated greedily to 10 000 bps. The last included partner may receive a partial fill (fewer bps than requested).
  3. Any remaining bps go to the buffer vault, funded from bufferDeposit. Each partner's cost = floor(currentPrice × allocatedWeight / 10 000). The buffer vault's bps share plus arithmetic rounding dust are both deducted from bufferDeposit, keeping per-partner costs strictly proportional. Buffer vault ──────────── A protocol-owned vault that absorbs small remainders when partners nearly (but not exactly) fill the board. It is not intended to force a close — a round may legitimately expire without triggering. The buffer is sized to bridge gaps of roughly 5–10 % of the board; it is subject to BeraChef's per-vault weight cap. bufferDeposit rolls over between rounds and is never refunded on expiry. Even when the buffer vault receives zero bps (partners fill all 10 000 bps exactly), bufferDeposit must cover up to (partnerCount − 1) wei of rounding dust arising from floor-dividing the auction price across allocations. Governance should maintain a minimum deposit of at least maxNumWeightsPerRewardAllocation wei to guarantee this is always satisfied. Access control ────────────── GOVERNANCE_ROLE: setBufferVault, withdrawBuffer, setMinSlotWeight, setSlotNFT, recoverERC20, unpause PAUSER_ROLE: pause All other operations are permissionless.

State Variables

CUTTING_BOARD_SYNDICATE_STORAGE_LOCATION

bytes32 private constant CUTTING_BOARD_SYNDICATE_STORAGE_LOCATION =
    0xfc0b30dd43c7ca556e4ca78929e2b1fe291ff15419dda9ea9db08f129f339e00

Functions

_getStorage

function _getStorage()
    private
    pure
    returns (CuttingBoardSyndicateLib
                .CuttingBoardSyndicateStorage storage $);

constructor

Note: oz-upgrades-unsafe-allow: constructor

constructor() ;

initialize

Initialise the syndicate.

function initialize(InitParams calldata params) external initializer;

Parameters

NameTypeDescription
paramsInitParamsConfiguration parameters.

dutchAuction

function dutchAuction()
    public
    view
    virtual
    returns (CuttingBoardDutchAuction);

controlManager

function controlManager()
    public
    view
    virtual
    returns (CuttingBoardManager);

controlNFT

function controlNFT() public view virtual returns (CuttingBoardNFT);

paymentToken

function paymentToken() public view virtual returns (ERC20);

chef

function chef() public view virtual returns (IBeraChefVaultCheck);

minSlotWeight

function minSlotWeight() public view virtual returns (uint96);

bufferVault

function bufferVault() public view virtual returns (address);

bufferDeposit

function bufferDeposit() public view virtual returns (uint256);

slotNFT

function slotNFT() public view virtual returns (CuttingBoardSlotNFT);

minimumPricePerBps

Minimum acceptable maxPricePerBps derived from the auction's minimumPrice.

Returns dutchAuction.minimumPrice() / 10 000 — the floor per-bps price.

function minimumPricePerBps() public view virtual returns (uint256);

getPendingRefund

Pending refund balance for a single user across all rounds

function getPendingRefund(address user)
    public
    view
    virtual
    returns (uint256);

getPendingRefunds

All addresses with a non-zero pending refund and their balances.

Enumerates the refundees tracking array. Claimed users are removed on claim, so every entry has a non-zero balance.

function getPendingRefunds()
    external
    view
    virtual
    returns (address[] memory users, uint256[] memory amounts);

getRoundBufferAllocated

Buffer vault's allocated weight for a given Active round (0 otherwise)

function getRoundBufferAllocated(uint256 auctionId)
    public
    view
    virtual
    returns (uint96);

setBufferVault

Set the fallback vault that absorbs unallocated bps.

Only callable by governance. Intended for bridging small gaps (~5–10 % of the board) when partners nearly but not exactly fill 10 000 bps. Subject to BeraChef's per-vault weight cap — set the buffer deposit to cover at most maxWeightPerVault bps per round. Set to address(0) to disable. WARNING: Do not set to a vault that is currently registered by any partner in an Open round. If the same vault appears twice in the weight assembly, BeraChef will reject the claim and the round will be stuck until the conflict is resolved (partner updates their vault or governance changes bufferVault) or the auction window expires.

function setBufferVault(address vault) external virtual onlyGovernor;

updateRoundBufferVault

Replace the buffer vault for a specific Active round.

Governance-only. Use when BeraChef de-whitelists the buffer vault that was snapshotted at trigger time, which would otherwise block all proposals for that round. The new vault must be whitelisted and must not duplicate any included partner's vault in the round. Only meaningful when roundBufferAllocated[auctionId] > 0 — if the buffer has no allocation the vault is not included in proposals.

function updateRoundBufferVault(uint256 auctionId, address newVault)
    external
    virtual
    onlyGovernor;

Parameters

NameTypeDescription
auctionIduint256The Active round to update.
newVaultaddressThe replacement buffer vault address.

withdrawBuffer

Withdraw tokens from the buffer reserve.

Only callable by governance. Reverts on underflow.

function withdrawBuffer(address to, uint256 amount)
    external
    virtual
    onlyGovernor;

setMinSlotWeight

Update the minimum weight (and allocation step size) for partner slots.

Must evenly divide 10 000 so partial fills are always valid multiples. For example 100 (1 %), 200 (2 %), 500 (5 %) are valid; 300 is not.

function setMinSlotWeight(uint96 minWeight) external virtual onlyGovernor;

setSlotNFT

Set the per-winner slot NFT contract.

Only callable by governance. Deploy CuttingBoardSlotNFT with this syndicate's address as the authorised minter, then call this function to activate the feature.

function setSlotNFT(address _slotNFT) external virtual onlyGovernor;

Parameters

NameTypeDescription
_slotNFTaddressAddress of the CuttingBoardSlotNFT contract.

recoverERC20

Recover accidentally sent ERC-20 tokens.

Only callable by governance. Reverts if token is the payment token to prevent draining user deposits and buffer funds.

function recoverERC20(address token, address to, uint256 amount)
    external
    virtual
    onlyGovernor;

Parameters

NameTypeDescription
tokenaddressERC-20 token to recover.
toaddressRecipient address.
amountuint256Amount to transfer.

depositBuffer

Deposit payment tokens into the buffer reserve.

Permissionless. The reserve persists across rounds and is consumed only at claim time (proportional to the buffer's bps allocation, plus any rounding dust from floor-dividing the price across partner allocations).

function depositBuffer(uint256 amount) external virtual;

openRound

Open partner registration for a specific auction.

Permissionless. Each auctionId can only be opened once. Rounds for different auctions — including concurrent auctions across validators or overlapping allocation periods for the same validator — are fully independent and do not block each other. validatorHash is derived on-chain from the auction contract so there is no risk of mismatch between auctionId and validator.

function openRound(uint256 auctionId) external virtual whenNotPaused;

Parameters

NameTypeDescription
auctionIduint256The auction ID in CuttingBoardDutchAuction to target.

registerSlot

Register a slot in an Open round.

function registerSlot(
    uint256 auctionId,
    address vault,
    uint96 weight,
    uint128 maxPricePerBps
) external virtual whenNotPaused;

Parameters

NameTypeDescription
auctionIduint256Round to register in.
vaultaddressWhitelisted BeraChef receiver vault. Must not equal bufferVault. Multiple partners may bid on the same vault; at trigger time only the top bid per vault wins.
weightuint96Bps to bid for. Must be a positive multiple of minSlotWeight and at most maxWeightPerVault. Partners may collectively request more than 10 000 bps; actual allocation is resolved at trigger time.
maxPricePerBpsuint128Maximum price per basis point this partner will accept. Must be ≥ minimumPricePerBps(). Deposit = maxPricePerBps × weight is collected now.

updateSlotVault

Change the vault for a slot (Open state convenience overload).

In Open state, msg.sender is the partner key. Reverts if the round is not Open; callers in Active state must use the three-argument overload so they can specify which slot they hold the SlotNFT for.

function updateSlotVault(uint256 auctionId, address newVault)
    external
    virtual
    whenNotPaused;

updateSlotVault

Change the vault for a slot in Active state via SlotNFT ownership, then automatically submit an updated cutting board proposal.

The caller must hold the SlotNFT for (auctionId, originalPartner). This explicit parameter avoids an O(n) scan and supports callers who hold multiple SlotNFTs in the same round (acquired on secondary market). The SlotNFT metadata is kept in sync with the slot storage. After updating the vault, the function assembles the current weight allocation and submits a proposal to CuttingBoardManager. The manager's keeper must still call approveCuttingBoard() to execute the update on-chain via Infrared. The new vault must be whitelisted, must not equal bufferVault, and must not duplicate any other partner's current vault choice.

function updateSlotVault(
    uint256 auctionId,
    address originalPartner,
    address newVault
) external virtual whenNotPaused;

Parameters

NameTypeDescription
auctionIduint256The round to update.
originalPartneraddressThe original partner who registered the slot.
newVaultaddressThe new BeraChef receiver vault.

increaseMaxPrice

Raise your maximum acceptable price, topping up deposit accordingly.

Only upward adjustment is permitted. Downward adjustment would allow a free-rider exploit: register at a high ceiling to gain fill priority, then lower to extract an oversized refund at other partners' expense.

function increaseMaxPrice(uint256 auctionId, uint128 newMaxPricePerBps)
    external
    virtual
    whenNotPaused;

triggerClaim

Trigger the auction claim once fill conditions are satisfied. Conditions at currentPrice:

  1. At least one partner is eligible (maxPricePerBps × 10 000 ≥ currentPrice).
  2. The buffer vault's share (10 000 − partnerFill) does not exceed BeraChef's per-vault weight cap.
  3. bufferDeposit covers bufferRequired (buffer vault's bps cost plus rounding dust from floor-dividing price across allocations). This applies even when the buffer vault receives zero bps. On success:
  • Syndicate pays the auction and receives the CuttingBoardNFT.
  • Each included partner's excess deposit is credited to pendingRefunds.
  • Each excluded partner's full deposit is credited to pendingRefunds.
  • Round state transitions to Active.

Permissionless. External parties may also bid directly on the underlying auction. If they claim first the round can no longer be triggered and expireRound() should be called to refund partners.

function triggerClaim(uint256 auctionId) external virtual whenNotPaused;

_mintSlotNFTs

Mint a SlotNFT for each included partner. Extracted from triggerClaim() to avoid stack-too-deep; operates in its own stack frame.

function _mintSlotNFTs(
    CuttingBoardSyndicateLib.CuttingBoardSyndicateStorage storage $,
    CuttingBoardSlotNFT _slotNFT,
    uint256 auctionId,
    uint256 currentPrice,
    uint96 controlTokenId,
    CuttingBoardSyndicateLib.FillResult memory fill
) private;

expireRound

Mark the round as Expired when the auction lapsed without a claim.

All partner deposits are moved to pendingRefunds atomically. This also handles the case where an external party claimed the auction first — isAuctionActive() returns false in both cases. bufferDeposit rolls over; it is not refunded on expiry. Not gated by whenNotPaused so partners can always recover their funds.

function expireRound(uint256 auctionId) external virtual;

completeRound

Transition an Active round to Complete once its control NFT has expired.

Permissionless — anyone can call once the NFT is no longer valid.

function completeRound(uint256 auctionId) external virtual;

Parameters

NameTypeDescription
auctionIduint256The round to complete.

forceUpdateSlotVault

Governance override to replace a partner's vault when the SlotNFT holder is unresponsive and the vault has been de-whitelisted.

Bypasses SlotNFT ownership check. Updates slot storage and SlotNFT metadata, then auto-submits an updated proposal to CuttingBoardManager. Use only when the holder cannot or will not call updateSlotVault themselves.

function forceUpdateSlotVault(
    uint256 auctionId,
    address originalPartner,
    address newVault
) external virtual onlyGovernor;

Parameters

NameTypeDescription
auctionIduint256The Active round to update.
originalPartneraddressThe partner whose vault is being replaced.
newVaultaddressThe replacement BeraChef receiver vault.

refreshProposal

Re-submit the current allocation as a proposal to prevent staleness from BeraChef's inactivity span.

Permissionless. Calls proposeCuttingBoard on the Manager using weightsFromStorage — does not change any weights. The Manager's keeper must still approve for the refresh to take effect on-chain. Only callable when the round is Active and the control NFT is valid.

function refreshProposal(uint256 auctionId)
    external
    virtual
    whenNotPaused
    onlyKeeper;

Parameters

NameTypeDescription
auctionIduint256The Active round to refresh.

claimRefund

Withdraw accumulated refunds for user.

Funds are sent to user, not msg.sender. Permissionless — anyone can push refunds to inactive partners (or pass their own address). Not gated by whenNotPaused — partners can always withdraw their funds.

function claimRefund(address user) external virtual;

claimAllRefunds

Push pending refunds to every address that has a non-zero balance.

Permissionless — useful for keepers to sweep the full ledger in one tx so partners do not need to call claimRefund() individually. Not gated by whenNotPaused — partners can always withdraw their funds. Iterates backwards and pops each entry so the array is empty when done.

function claimAllRefunds() external virtual;

isRoundAllocationValid

Check whether an Active round's stored allocation still satisfies current BeraChef rules (whitelist, maxWeightPerVault, maxNumWeightsPerRewardAllocation).

Use for monitoring: if valid is false, the allocation may have been silently displaced by BeraChef's default allocation.

function isRoundAllocationValid(uint256 auctionId)
    external
    view
    virtual
    returns (bool valid);

Returns

NameTypeDescription
validboolTrue if the allocation would pass BeraChef validation.

getRound

Round metadata for a given auction

function getRound(uint256 auctionId)
    external
    view
    virtual
    returns (CuttingBoardSyndicateLib.Round memory);

getPartners

Ordered partner list for a given round

function getPartners(uint256 auctionId)
    external
    view
    virtual
    returns (address[] memory);

getSlot

Slot details for a specific partner in a given round

function getSlot(uint256 auctionId, address partner)
    external
    view
    virtual
    returns (CuttingBoardSyndicateLib.Slot memory);

canTrigger

Returns true if triggerClaim(auctionId) would currently succeed.

Uses the same computeFill DELEGATECALL and validation predicates as triggerClaim itself, so the two functions are always aligned on feasibility — including live BeraChef policy (vault whitelist, maxWeightPerVault, maxNumWeightsPerRewardAllocation).

function canTrigger(uint256 auctionId)
    external
    view
    virtual
    returns (bool);

previewFillAt

Preview the fill outcome at a given auction price.

function previewFillAt(uint256 auctionId, uint256 price)
    external
    view
    virtual
    returns (
        address[] memory included,
        uint96[] memory allocations,
        uint256 count,
        uint96 bfWeight,
        uint256 bufferRequired
    );

Parameters

NameTypeDescription
auctionIduint256Round to preview.
priceuint256Hypothetical auction price.

Returns

NameTypeDescription
includedaddress[]Partners in fill order, trimmed to fill count.
allocationsuint96[]Allocated bps per partner, trimmed to fill count.
countuint256Number of included partners.
bfWeightuint96Bps assigned to the buffer vault.
bufferRequireduint256Payment tokens the buffer must supply (bps cost + rounding dust).

_creditRefund

Credit a refund to a user and register them in the enumeration set.

function _creditRefund(address user, uint256 amount) internal;

_claimRefund

Zero balance, remove from set, transfer, and emit.

function _claimRefund(address user) internal;

_removeRefundee

Remove a user from the refundees enumeration set (swap-and-pop).

function _removeRefundee(
    CuttingBoardSyndicateLib.CuttingBoardSyndicateStorage storage $,
    address user
) internal;

_clearSlots

function _clearSlots(uint256 auctionId) internal;

Events

BufferVaultSet

event BufferVaultSet(address indexed vault);

RoundBufferVaultUpdated

event RoundBufferVaultUpdated(
    uint256 indexed auctionId, address indexed newVault
);

BufferDeposited

event BufferDeposited(address indexed depositor, uint256 amount);

BufferWithdrawn

event BufferWithdrawn(address indexed recipient, uint256 amount);

MinSlotWeightSet

event MinSlotWeightSet(uint96 minWeight);

RoundOpened

event RoundOpened(uint256 indexed auctionId, bytes32 indexed validatorHash);

SlotRegistered

event SlotRegistered(
    uint256 indexed auctionId,
    address indexed partner,
    address indexed vault,
    uint96 weight,
    uint128 maxPricePerBps,
    uint128 deposit
);

SlotVaultUpdated

event SlotVaultUpdated(
    uint256 indexed auctionId,
    address indexed partner,
    address indexed newVault
);

MaxPriceIncreased

event MaxPriceIncreased(
    uint256 indexed auctionId,
    address indexed partner,
    uint128 newMax,
    uint128 additionalDeposit
);

SlotExited

event SlotExited(
    uint256 indexed auctionId, address indexed partner, uint128 refund
);

SlotFilled

Emitted for each partner included in the winning allocation

event SlotFilled(
    uint256 indexed auctionId,
    address indexed partner,
    uint96 requested,
    uint96 allocated,
    uint256 cost
);

SlotExcluded

Emitted for each partner whose price ceiling was below the trigger price

event SlotExcluded(
    uint256 indexed auctionId, address indexed partner, uint256 refund
);

BufferUsed

Emitted when the buffer vault absorbs unallocated bps

event BufferUsed(
    uint256 indexed auctionId,
    address indexed vault,
    uint96 weight,
    uint256 cost
);

RoundTriggered

event RoundTriggered(
    uint256 indexed auctionId, uint128 claimPrice, uint256 tokenId
);

RoundExpired

event RoundExpired(uint256 indexed auctionId);

RefundClaimed

event RefundClaimed(address indexed partner, uint256 amount);

RoundCompleted

event RoundCompleted(uint256 indexed auctionId);

SlotNFTSet

event SlotNFTSet(address indexed slotNFT);

ERC20Recovered

event ERC20Recovered(
    address indexed token, address indexed to, uint256 amount
);

Errors

NotOpen

error NotOpen();

NotActive

error NotActive();

RoundAlreadyExists

error RoundAlreadyExists();

SlotAlreadyExists

error SlotAlreadyExists();

NoSlot

error NoSlot();

InvalidWeight

Weight is zero, not a multiple of minSlotWeight, or exceeds the per-vault cap

error InvalidWeight();

InsufficientMaxPrice

maxPricePerBps must be ≥ minimumPricePerBps()

error InsufficientMaxPrice();

TooManyPartners

Included partners (+ buffer) exceed BeraChef's maxNumWeightsPerRewardAllocation

error TooManyPartners();

VaultNotWhitelisted

error VaultNotWhitelisted();

DuplicateVaultEntry

Vault is already registered by another partner or equals bufferVault

error DuplicateVaultEntry(address vault);

AuctionNotActive

error AuctionNotActive();

AuctionStillLive

error AuctionStillLive();

NFTExpiredOrInvalid

error NFTExpiredOrInvalid();

MaxPriceMustIncrease

error MaxPriceMustIncrease();

NothingToRefund

error NothingToRefund();

ZeroAddress

error ZeroAddress();

InvalidMinSlotWeight

minSlotWeight must be a positive divisor of 10 000

error InvalidMinSlotWeight();

NoBufferVault

bufferVault must be set before triggerClaim can allocate remaining bps to it

error NoBufferVault();

InsufficientBuffer

bufferDeposit is too small to cover the buffer vault's share plus rounding dust

error InsufficientBuffer();

NoBidders

No eligible partner has maxPricePerBps × 10 000 ≥ currentPrice

error NoBidders();

BufferWeightExceedsMax

Buffer vault would receive more than BeraChef's per-vault cap; more partner coverage is needed to reduce the buffer's share

error BufferWeightExceedsMax();

CannotRecoverPaymentToken

Cannot recover paymentToken via recoverERC20; use withdrawBuffer or claimRefund

error CannotRecoverPaymentToken();

DepositOverflow

maxPricePerBps × weight exceeds uint128; use a lower maxPricePerBps

error DepositOverflow();

SlotNFTNotSet

slotNFT must be set before calling updateSlotVault

error SlotNFTNotSet();

NotSlotNFTHolder

Caller does not own the SlotNFT for the requested tokenId

error NotSlotNFTHolder();

InvalidSlotNFT

SlotNFT address is not a compatible contract

error InvalidSlotNFT();

SlotNFTAlreadySet

SlotNFT address can only be set once

error SlotNFTAlreadySet();

BufferVaultNotWhitelisted

Buffer vault is no longer whitelisted in BeraChef

error BufferVaultNotWhitelisted();

NFTStillValid

error NFTStillValid();

Structs

InitParams

Parameters for initialize() — grouped to avoid stack-too-deep

struct InitParams {
    address dutchAuction;
    address controlManager;
    address controlNFT;
    address chef;
    address governance;
    address keeper; // optional; address(0) skips granting KEEPER_ROLE
    uint96 minSlotWeight; // 0 defaults to 100 (1 %)
}