Restricted Swap
Overview
RestrictedSwap provides a secure means of swapping tokens between known holders of the primary Restricted token and any payment ERC-20 tokens (ie USDC, DAI, etc).
Note: Restricted Swap is not intended between two Restricted tokens. Rather, it is intended as a purchase of Restricted tokens using ERC-20 tokens (AKA payment token or quote token). This is enforced in the Restricted Swap contract itself, by preventing ERC-1404 compatible tokens (checked via ERC-165 interface support) from being used as payment. However, other types of Restricted tokens may not explicitly support the ERC-1404 interface and thus cannot be checked programmatically; therefore, this functionality should be used with caution and payment token types should always be verified first.
Supported Quote Tokens
The swap contract guarantees the recipient's side of every quote-token transfer, and only that
side. Each settlement path measures the recipient's balance delta around the transfer and reverts
RestrictedSwap_InconsistentQuoteTokenAmount unless it equals the quoted amount exactly, so a
seller is always paid to the wei.
This rules out the common fee-on-transfer shape, where the fee is carved out of the transferred
value and the recipient receives less than was sent (USDT's basisPointsRate when enabled,
reflection tokens). Rebasing tokens are likewise unusable, since their balances move for reasons
other than the transfer.
The payer's debit is deliberately not measured. A quote token that surcharges the sender on top
of the transferred value — debiting amount + fee while crediting the recipient amount — settles
successfully, and the payer's real cost is whatever that token does. Nothing in the contract depends
on it: no funds are custodied, and every internal counter is denominated in the quoted amount, which
is exactly what the recipient receives.
Integrator constraint.
quoteTokenAmountis what the recipient receives, not necessarily what the payer is debited. Before presenting a swap's price to the paying side, check the quote token's transfer semantics — for a surcharging token the real price is higher than the ratio the contract advertises, and neither theSwapConfiguredevent nor the view helpers will show the difference. Verifying the payment token is part of listing an order, as it is for the ERC-1404 check described above.
Usage
How It Works
As a prerequisite, users of the swap functionality are assumed to have passed any necessary off-chain AML/KYC and deemed valid holders. Contracts are also assumed to have been configured properly with admin-granted roles and transfer restrictions in place.
Configuring a Purchase of Restricted Token
As a holder (Buyer) of an ERC-20 payment token (ie USDC, DAI, etc), one can configure a purchase order for a specified amount of Restricted token with a known party (Seller). Here is an example involving USDC:
Buyermust know in advance specifically how much Restricted token they'd like to receive, how much USDC they are willing to pay, and theSelleron the other end.Buyermust approve the Swap contract itself to handle their USDC in the purchase amount required.Buyerthen configures an open purchase order by callingconfigureBuywith the following parameters. A Swap ID is emitted in an event.- amount of Restricted token desired
- address of
Seller - address of payment token contract
- amount of payment token (USDC) willing to swap
- deadline for swap expiration (required; must be in the future and within the contract's
maxSwapLifetime) - minimum fill amount (must be
0for this closed order)
Sellermust approve the Swap contract itself to handle their Restricted token in the sell amount required by the configured swap.Sellercan complete this order by callingcompleteSwapWithRestrictedTokenwith the emitted Swap ID (passing0for the optional parent sell offer, since this bid does not draw from one).
When transfer restrictions are validated between Buyer and Seller, the swap is completed and assets are actually transferred between transacting parties.
Note: Role checks (e.g., pausing, admin-only operations) are resolved through the pre-deployed AccessControl contract referenced by the Restricted token and ancillary contracts.
Configuring a Sale of Restricted Token
As a holder of the Restricted token, one can also configure a sell order for a specified amount of desired payment ERC-20 token. It is extremely similar to the example above, except terms of the sale must be specified in advance. Here is an example involving USDC:
Sellermust know in advance specifically how much Restricted token they'd like to sell, how much USDC they require, and theBuyeron the other end.Sellermust approve the Swap contract itself to handle their Restricted token in the sell amount desired.Sellerthen configures an open sell order by callingconfigureSellwith the following parameters. A Swap ID is emitted in an event.- amount of Restricted token to sell
- address of payment token contract
- address of
Buyer - amount of payment token (USDC) required
- deadline for swap expiration (required; must be in the future and within the contract's
maxSwapLifetime) - minimum fill amount (must be
0for this closed order)
Buyermust approve the Swap contract itself to handle their USDC in the amount specified by the configured swap.Buyercan complete this order by callingcompleteSwapWithQuoteTokenwith the emitted Swap ID.
When transfer restrictions are validated between Buyer and Seller, the swap is completed and assets are actually transferred between transacting parties.
Open Orders and Partial Fills
In addition to the closed (named-counterparty) swap flows above, RestrictedSwap supports open orders that any compliant counterparty can fill, with optional partial fills so a single listing can be consumed by multiple takers.
How It Works
- Open Sell Order: call
configureSell(...)withquoteTokenSender = address(0). The seller's restricted-token allowance is escrowed viarequiredAllowance, and any compliant buyer can calltakeOpenSell(swapNumber, fillAmount)(ortakeOpenSellWithPermit(...)to bundle ERC-2612 approval) to settle. - Open Buy Order: symmetric — call
configureBuy(...)withrestrictedTokenSender = address(0). The buyer's quote-token allowance is escrowed and any compliant holder may calltakeOpenBuy(...)/takeOpenBuyWithPermit(...).
Compliance (detectTransferRestriction) is deferred to take time for open orders because the counterparty is unknown at configure time. The check runs against the actual taker before the transfer.
Partial Fills
Takers may specify a fillAmount less than the remaining restricted-token amount. The pro-rata quote charge is computed as quoteTokenAmount * fillAmount / restrictedTokenAmount and must be a whole number — the contract reverts RestrictedSwap_AmountNotDivisible on any fillAmount that does not divide cleanly into the price ratio. This rules out rounding entirely: every fill pays its exact pro-rata share, no taker can ever overpay or underpay by even one wei, and no inventory can be siphoned via dust-fill griefing.
⚠️ Pick listing amounts with a large GCD
Because the pro-rata charge must be exact, the set of valid fillAmount values for a listing is fixed by the GCD of quoteTokenAmount and restrictedTokenAmount:
Valid
fillAmount∈ multiples ofrestrictedTokenAmount / gcd(quoteTokenAmount, restrictedTokenAmount), up toremainingRestrictedTokenAmount.
Listing (restricted / quote) | GCD | Smallest valid fill | Outcome |
|---|---|---|---|
1000 RLT for 100 USDC | 100 | 10 RLT | Up to 100 partial fills possible |
1000 RLT for 250 USDC | 250 | 4 RLT | Up to 250 partial fills possible |
1000 RLT for 7 USDC | 1 | 1000 RLT | ❗ Effectively no partial fills — only a full take works |
997 RLT for 11 USDC (both prime) | 1 | 997 RLT | ❗ Coprime — only a full take works |
Coprime amounts silently force a single full-listing take. Sellers/buyers configuring open orders should:
- Round listing amounts to ratios with a large GCD (e.g. multiples of 10 / 100 / 1000 of the smaller side), or
- Accept that the listing is take-it-or-leave-it.
Off-chain orderbook UIs SHOULD validate the listing's GCD at configure time and surface a warning, since the constraint is not enforced on configureSell / configureBuy — only at fill time.
Cancellation refunds only the remaining unfilled amount, not the original; already-filled portions are not reversed.
Self-Fill Disallowed
A configurer cannot also be the counterparty to their own open order. takeOpenSell reverts with RestrictedSwap_SelfFillNotAllowed when _msgSender() == swap.restrictedTokenSender; takeOpenBuy reverts the same way when _msgSender() == swap.quoteTokenSender. This prevents wash fills, misleading OpenSwapFilled events, and using takeOpen* as a cheaper substitute for cancelSwap. Configurers who want to retire an unfilled listing must call cancelSwap.
Events
Each partial OR final fill emits:
OpenSwapFilled(swapNumber, filler, fillRestrictedAmount, fillQuoteAmount, remainingRestrictedAmount)
remainingRestrictedAmount == 0 indicates the order is now fully filled; the contract additionally emits a regular SwapComplete with the last filler in the counterparty slot.
Indexer Notes
- An open order is recognizable by
SwapConfiguredcarryingquoteTokenSender == address(0)(open sell) orrestrictedTokenSender == address(0)(open buy). - A single open order may emit multiple
OpenSwapFilledevents before its finalSwapComplete. - Off-chain orderbooks coordinating concurrent takers should serialize submissions per listing (
SELECT FOR UPDATE) to avoid racing on the same on-chain remaining amount; lost races revert withRestrictedSwap_InvalidFillAmountorRestrictedSwap_AlreadyCompleted.
View Helpers
remainingRestrictedTokenAmount(swapNumber)— current unfilled restricted amount. On aCompleteswap this is always0; aCanceledswap keeps the unfilled tail as history, so pair it withswapStatusbefore treating a non-zero value as available inventory.isOpenOrder(swapNumber)— true if the swap has a zero counterparty (open) and must be filled viatakeOpenSell/takeOpenBuy.
Deadline Functionality
Every swap carries a mandatory deadline that automatically expires it after a specified timestamp. This prevents indefinitely active orders and bounds how long allowances stay reserved:
- Required Deadline:
deadlinemust be> 0at configure time — the "no deadline" (0) sentinel is rejected withRestrictedSwap_DeadlineRequired. - Must Be In The Future: A
deadlinein the past reverts withRestrictedSwap_SwapExpiredat configure time. - Bounded Horizon:
deadline - block.timestampmust not exceed the contract's immutablemaxSwapLifetime, or configuration reverts withRestrictedSwap_DeadlineExceedsMaxLifetime.maxSwapLifetimeis fixed at deployment within[MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT]. - Automatic Expiration: Once a swap's deadline passes, it becomes invalid and cannot be completed or filled.
- Anyone Can Cancel Expired Swaps: Once a swap expires, anyone can cancel it to free up allowances.
Key Features:
- Deadline is checked during swap completion - expired swaps cannot be completed
- Expired swaps can be canceled by anyone, not just the original parties
- The
isSwapExpired()function allows checking if a swap has expired - All swap events include the deadline parameter for transparency
EIP-2612 Permit Helpers
Every ...WithPermit entry point bundles an EIP-2612 permit with the operation it performs, so a caller signs one ERC-2771 forward request instead of sending a separate approve first.
Sizing permitValue
permitValue is the value the caller signed. It is forwarded to permit verbatim and never recomputed on-chain, because an EIP-2612 signature binds the exact value: a contract that derived the value from live reservation state would invalidate every signature that drifted before it was relayed — including drift caused by an honest third party filling one of the caller's other open orders.
Size it off-chain as:
permitValue = requiredAllowance(caller, token) + <this operation's delta>
Rules that follow from permit overwriting (not incrementing) the allowance:
- Cover existing reservations, not just this operation. A delta-only value would clobber the allowance backing the caller's other active orders, leaving them unfillable and out of sync with
requiredAllowance. - The requirement is re-evaluated at execution time.
permitValueonly has to be at least the requirement when the transaction lands; the contract rechecks the resulting allowance and revertsRestrictedSwap_Insufficient{Quote,Restricted}TokenAllowanceAfterPermitif it is still short. Adding headroom is therefore the safe direction on an active order book. - Over-signing is allowed but not free. A value far above the requirement (e.g.
type(uint256).max) forfeits the exact-sizing property the contract otherwise maintains, leaving the caller authorised beyond their remaining intent. - The permit is skipped when it is not needed. If the standing allowance already covers the requirement,
permitis never called — so a value signed for exactly the requirement cannot trim headroom the caller set deliberately, and the signature's nonce is left unconsumed. - A replayed permit cannot grief the call. The
permitcall is wrapped intry/catchand followed by the allowance recheck, so a front-runner who submits the signature first (consuming the nonce) does not break the operation.
API Reference
Constructor
constructor(address restrictedLockupTokenAddress_, address trustedForwarder_, address accessControl_, uint256 maxSwapLifetime_)
Initializes the RestrictedSwap contract with configuration parameters.
Parameters:
restrictedLockupTokenAddress_(address): The RestrictedLockupToken contract addresstrustedForwarder_(address): ERC-2771 trusted forwarder address for meta-transactionsaccessControl_(address): AccessControl contract address for role managementmaxSwapLifetime_(uint256): Maximum allowed distance betweenblock.timestampand a swap'sdeadlineat configure time, fixed for the life of the contract
Requirements:
- All addresses must be non-zero
- Access control address must be valid
maxSwapLifetime_must be within[MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT], elseRestrictedSwap_InvalidMaxSwapLifetime
Administrative Functions
pause(bool isPaused_)
Pauses or unpauses the contract functionality.
Parameters:
isPaused_(bool): True to pause, false to unpause
Requirements:
- Caller must have CONTRACT_ADMIN_ROLE or TRANSFER_ADMIN_ROLE
Emits: Paused(account) or Unpaused(account)
Swap Configuration Functions
configureSell(uint256 restrictedTokenAmount, address quoteToken, address quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)
Configures a sell order for restricted tokens in exchange for quote tokens.
Parameters:
restrictedTokenAmount(uint256): Amount of restricted tokens to sellquoteToken(address): Address of the quote token (ERC-20) to receivequoteTokenSender(address): Address that will provide the quote tokens, oraddress(0)to create an open sell order any compliant buyer can fill viatakeOpenSellquoteTokenAmount(uint256): Amount of quote tokens to receivedeadline(uint256): Unix timestamp deadline for swap expiration (required; see Deadline Functionality)minimumFillAmount(uint256): Minimum restricted-token amount a taker must fill on an open order viatakeOpenSell(unless taking the entire remaining amount);0means no minimum. Must be0for closed swaps.
Requirements:
- Contract must not be paused
- Quote token must not be zero address
- Caller must have sufficient restricted token allowance for this contract
- Quote token must not support ERC-1404 interface (to prevent restricted token swaps)
deadlinemust be> 0, not in the past, and withinmaxSwapLifetimeminimumFillAmountmust be<= restrictedTokenAmount, and must be0whenquoteTokenSenderis a concrete address (closed swap)
Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)
configureBuy(uint256 restrictedTokenAmount, address restrictedTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)
Configures a buy order for restricted tokens using quote tokens.
Parameters:
restrictedTokenAmount(uint256): Amount of restricted tokens to buyrestrictedTokenSender(address): Address that will provide the restricted tokens, oraddress(0)to create an open buy order any compliant holder can fill viatakeOpenBuyquoteToken(address): Address of the quote token (ERC-20) to pay withquoteTokenAmount(uint256): Amount of quote tokens to paydeadline(uint256): Unix timestamp deadline for swap expiration (required; see Deadline Functionality)minimumFillAmount(uint256): Minimum restricted-token amount a taker must fill on an open order viatakeOpenBuy(unless taking the entire remaining amount);0means no minimum. Must be0for closed swaps.
Requirements:
- Contract must not be paused
- Quote token must not be zero address
- Caller must have sufficient quote token balance
- Caller must have sufficient quote token allowance for this contract
- Quote token must not support ERC-1404 interface
deadlinemust be> 0, not in the past, and withinmaxSwapLifetimeminimumFillAmountmust be<= restrictedTokenAmount, and must be0whenrestrictedTokenSenderis a concrete address (closed swap)
Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)
configureSellWithPermit(uint256 restrictedTokenAmount, address quoteToken, address quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as configureSell, but approves the restricted-token allowance via an EIP-2612 permit signature so approve + configure land in a single (optionally relayer-driven) transaction.
Parameters:
- All parameters of
configureSell, plus: permitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
configureSell - The restricted token must implement EIP-2612
permitValuemust coverrequiredAllowance(caller, restrictedToken) + restrictedTokenAmountas of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, revertingRestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermitif still short
Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)
configureBuyWithPermit(uint256 restrictedTokenAmount, address restrictedTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as configureBuy, but approves the quote-token allowance via an EIP-2612 permit signature so approve + configure land in a single (optionally relayer-driven) transaction.
Parameters:
- All parameters of
configureBuy, plus: permitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
configureBuy - The quote token must implement EIP-2612
permitValuemust coverrequiredAllowance(caller, quoteToken) + quoteTokenAmountas of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, revertingRestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermitif still short
Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)
Swap Execution Functions
completeSwapWithQuoteToken(uint256 swapNumber_)
Completes a swap by providing quote tokens (used by quote token sender in sell-configured swaps).
Parameters:
swapNumber_(uint256): The swap number to complete
Requirements:
- Contract must not be paused
- Swap must not be canceled or already completed
- Caller must be the quote token sender for this swap
- Swap status must be SellConfigured
- Quote token sender must have sufficient quote token balance
- Restricted token sender must have sufficient restricted token balance
- Transfer restrictions must be satisfied between parties
Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline)
completeSwapWithQuoteTokenPermit(uint256 swapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as completeSwapWithQuoteToken, but approves the quote-token allowance via an EIP-2612 permit signature so approve + complete land in a single transaction.
Parameters:
swapNumber_(uint256): The swap number to completepermitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
completeSwapWithQuoteToken - The quote token must implement EIP-2612
permitValuemust coverrequiredAllowance(caller, quoteToken) + quoteTokenAmountas of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, revertingRestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermitif still short
Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline)
completeSwapWithRestrictedToken(uint256 swapNumber_, uint256 sellSwapNumber_)
Completes a closed buy order (or an accepted bid) by providing restricted tokens (used by the restricted token sender in buy-configured swaps).
Parameters:
swapNumber_(uint256): The swap number of the bid/closed buy order to completesellSwapNumber_(uint256): The swap number of the caller's own open sell offer this settlement draws from, deducting the settled amount from that offer in the same transaction (emittingOrderResized); pass0for a standalone settlement that does not draw from an open offer
Requirements:
- Contract must not be paused
- Swap must not be canceled or already completed
- Caller must be the restricted token sender for this swap
- Swap status must be BuyConfigured
- Swap must be a closed buy order (open buy orders must be filled via
takeOpenBuy, elseRestrictedSwap_OpenOrderRequiresTake) - Quote token sender must have sufficient quote token balance
- Restricted token sender must have sufficient restricted token balance
- Transfer restrictions must be satisfied between parties
- When
sellSwapNumber_ != 0: it must reference an open sell offer owned by the caller, and the settled amount must not exceed that offer's remaining size (RestrictedSwap_ExceedsParentRemaining)
Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline); also OrderResized on the parent offer when sellSwapNumber_ != 0
completeSwapWithRestrictedTokenPermit(uint256 swapNumber_, uint256 sellSwapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as completeSwapWithRestrictedToken, but approves the restricted-token allowance via an EIP-2612 permit signature so approve + settle land in a single transaction.
Parameters:
swapNumber_(uint256): The swap number of the bid/closed buy order to completesellSwapNumber_(uint256): The caller's own open sell offer to deduct from, or0for a standalone settlementpermitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
completeSwapWithRestrictedToken - The restricted token must implement EIP-2612
permitValuesizing (permit overwrites the allowance): withsellSwapNumber_ == 0it must coverrequiredAllowance(caller, restrictedToken) + restrictedTokenAmount; with a parent offer named the delta is zero, sorequiredAllowance(caller, restrictedToken)suffices (the parent's reservation already covers the bid). Both measured as of execution time. The post-permit allowance is rechecked, revertingRestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermitif short
Emits: SwapComplete(...); also OrderResized on the parent offer when sellSwapNumber_ != 0
takeOpenSell(uint256 swapNumber_, uint256 fillAmount)
Fills (fully or partially) an open sell order configured with quoteTokenSender == address(0). The caller receives fillAmount restricted tokens and pays the exact pro-rata quoteTokenAmount * fillAmount / restrictedTokenAmount.
Parameters:
swapNumber_(uint256): The open sell order to fillfillAmount(uint256): Amount of restricted tokens to receive
Requirements:
- Contract must not be paused
- Swap must not be canceled or already completed
- Swap must be an open sell order, else
RestrictedSwap_NotOpenOrder - Caller must not be the order's
restrictedTokenSender, elseRestrictedSwap_SelfFillNotAllowed fillAmountmust be> 0and<= remainingRestrictedTokenAmount, elseRestrictedSwap_InvalidFillAmountfillAmountmust be>= minimumFillAmountunless it takes the entire remaining amount, elseRestrictedSwap_FillBelowMinimum- The pro-rata quote charge must be a whole number, else
RestrictedSwap_AmountNotDivisible - Caller must have sufficient quote token balance and allowance
- Transfer restrictions must be satisfied between parties (checked against the actual taker)
Emits: OpenSwapFilled(swapNumber, filler, fillRestrictedAmount, fillQuoteAmount, remainingRestrictedAmount); also SwapComplete(...) when the fill exhausts the order
takeOpenSellWithPermit(uint256 swapNumber_, uint256 fillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as takeOpenSell, but approves the quote-token allowance via an EIP-2612 permit signature.
Parameters:
swapNumber_(uint256): The open sell order to fillfillAmount(uint256): Amount of restricted tokens to receivepermitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
takeOpenSell - The quote token must implement EIP-2612
permitValuemust coverrequiredAllowance(caller, quoteToken) + fillQuoteAmountas of execution time (permit overwrites the allowance); rechecked afterwards, revertingRestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermitif short
Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order
takeOpenBuy(uint256 swapNumber_, uint256 fillAmount, uint256 sellSwapNumber_)
Fills (fully or partially) an open buy order configured with restrictedTokenSender == address(0). The caller delivers fillAmount restricted tokens and receives the exact pro-rata quote amount.
Parameters:
swapNumber_(uint256): The open buy order to fillfillAmount(uint256): Amount of restricted tokens to deliversellSwapNumber_(uint256): The caller's own open sell offer this delivery draws from, deductingfillAmountfrom that offer in the same transaction (emittingOrderResized); pass0when the fill draws from no open offer
Requirements:
- Contract must not be paused
- Swap must not be canceled or already completed
- Swap must be an open buy order, else
RestrictedSwap_NotOpenOrder - Caller must not be the order's
quoteTokenSender, elseRestrictedSwap_SelfFillNotAllowed fillAmountmust be> 0and<= remainingRestrictedTokenAmount, elseRestrictedSwap_InvalidFillAmountfillAmountmust be>= minimumFillAmountunless it takes the entire remaining amount, elseRestrictedSwap_FillBelowMinimum- The pro-rata quote credit must be a whole number, else
RestrictedSwap_AmountNotDivisible - Caller must have sufficient restricted token balance and allowance
- Transfer restrictions must be satisfied between parties (checked against the actual taker)
- When
sellSwapNumber_ != 0: it must reference an open sell offer owned by the caller,fillAmountmust divide cleanly against that offer's price ratio, and must not exceed its remaining size (RestrictedSwap_ExceedsParentRemaining)
Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order, and OrderResized on the parent offer when sellSwapNumber_ != 0
takeOpenBuyWithPermit(uint256 swapNumber_, uint256 fillAmount, uint256 sellSwapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as takeOpenBuy, but approves the restricted-token allowance via an EIP-2612 permit signature.
Parameters:
swapNumber_(uint256): The open buy order to fillfillAmount(uint256): Amount of restricted tokens to deliversellSwapNumber_(uint256): The caller's own open sell offer to deduct from, or0permitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
takeOpenBuy - The restricted token must implement EIP-2612
permitValuesizing (permit overwrites the allowance): withsellSwapNumber_ == 0signrequiredAllowance(caller, restrictedToken) + fillAmount; with a parent offer named the delta is zero, sorequiredAllowance(caller, restrictedToken)suffices. Both measured as of execution time. Rechecked afterwards, revertingRestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermitif short
Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order, and OrderResized on the parent offer when sellSwapNumber_ != 0
cancelSwap(uint256 swapNumber_)
Cancels a configured swap. For non-expired swaps, only the original parties can cancel. For expired swaps, anyone can cancel.
Parameters:
swapNumber_(uint256): The swap number to cancel
Requirements:
- Swap must not be canceled or already completed
- Swap must be properly configured
- For non-expired swaps:
- For SellConfigured swaps: caller must be the restricted token sender
- For BuyConfigured swaps: caller must be the quote token sender
- For expired swaps: anyone can cancel
Emits: SwapCanceled(sender, swapNumber)
Order Management Functions
decreaseOrder(uint256 swapNumber, uint256 newRemainingRestrictedAmount)
Shrinks an active open order's remaining size in place (owner-only, decrease-only) — the single-signature alternative to cancel-and-recreate. The paired quote amount shrinks exactly pro-rata on the order's original price. Shrinking to 0 cancels the order. Like cancelSwap, callable while paused and on expired orders (exit-type action).
Parameters:
swapNumber(uint256): The caller's open ordernewRemainingRestrictedAmount(uint256): New remaining restricted-token size; must be strictly less than the current remaining amount (0cancels the order)
Requirements:
- Caller must be the order's creator, else
RestrictedSwap_InvalidOrderOwner - Swap must be an open order, else
RestrictedSwap_NotOpenOrder - New size must be strictly smaller than the current remaining amount, else
RestrictedSwap_InvalidResizeAmount - The new quote remainder must be a whole number at the original price ratio, else
RestrictedSwap_AmountNotDivisible
Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount); also SwapCanceled(...) when shrunk to zero
increaseOrder(uint256 swapNumber, uint256 newRemainingRestrictedAmount)
Grows an active open order's remaining size in place (owner-only, increase-only). Because growing creates new exposure, this carries configure-grade guards: blocked while paused, blocked on expired orders, and the creator's allowance (and, for buys, quote balance) must already cover all reservations plus the growth. After an increase the remaining size may exceed the originally configured amount — derive traded totals from OpenSwapFilled events, not configured amounts.
Parameters:
swapNumber(uint256): The caller's open ordernewRemainingRestrictedAmount(uint256): New remaining restricted-token size; must be strictly greater than the current remaining amount
Requirements:
- Caller must be the order's creator, else
RestrictedSwap_InvalidOrderOwner - Contract must not be paused; order must not be expired
- Swap must be an open order, else
RestrictedSwap_NotOpenOrder - New size must be strictly greater than the current remaining amount, else
RestrictedSwap_InvalidResizeAmount - The added amount must divide cleanly against the order's original price ratio, else
RestrictedSwap_AmountNotDivisible - The creator's allowance must cover all reservations plus the growth (open buys also re-check quote balance against the order's new total)
Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount)
increaseOrderWithPermit(uint256 swapNumber, uint256 newRemainingRestrictedAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)
Same as increaseOrder, but tops up the allowance via an EIP-2612 permit signature so the top-up and the size increase land atomically. The permitted token is the restricted token for an open sell, or the order's quote token for an open buy.
Parameters:
swapNumber(uint256): The caller's open ordernewRemainingRestrictedAmount(uint256): New remaining restricted-token size (strictly greater than current)permitValue(uint256): The EIP-2612valuethe caller signed. Forwarded topermitverbatim and never recomputed on-chain — see SizingpermitValuepermitDeadline(uint256): EIP-2612 permit deadline (must be>= block.timestamp)v,r,s: Components of the permit signature
Requirements:
- Same as
increaseOrder - The permitted token must implement EIP-2612
permitValuemust coverrequiredAllowance + added(open sell, restricted token) orrequiredAllowance + quoteAdded(open buy, quote token) as of execution time, since permit overwrites the allowance. Only one side's permit is consumed, so a single signed value covers either branch
Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount)
Query Functions
swapNumber() → uint256
Returns the current swap number counter.
Returns:
uint256: The current swap number (total swaps configured)
swapStatus(uint256 swapNumber_) → SwapStatus
Returns the status of a specific swap.
Parameters:
swapNumber_(uint256): The swap number to query
Returns:
SwapStatus: The status enum (SellConfigured=1, BuyConfigured=2, Complete=3, Canceled=4)
Requirements:
- Swap record must exist
remainingRestrictedTokenAmount(uint256 swapNumber_) → uint256
Returns the restricted-token amount that was never filled on a swap. Only open orders decrement it (each partial fill, plus decreaseOrder / increaseOrder resizes); a closed swap settles its original amounts in full, so it reports its configure-time amount for its whole lifetime.
Terminal states:
Complete→ always0.Canceled→ retains the amount withdrawn unfilled, deliberately kept as on-chain history of what was pulled back rather than traded (the refund itself is visible inpendingSells/requiredAllowancedropping). The one exception is an order retired by shrinking to zero — viadecreaseOrder(swapNumber, 0)or a parent-offer deduction — where the shrink already consumed the remainder, so it reports0.
Read swapStatus(swapNumber) alongside this getter: a non-zero value on a terminal record is history, not available inventory.
Parameters:
swapNumber_(uint256): The swap number to query
Returns:
uint256: The unfilled restricted-token amount
minimumFillAmount(uint256 swapNumber_) → uint256
Returns the minimum restricted-token amount a taker must fill on an open order (unless taking the entire remaining amount).
Parameters:
swapNumber_(uint256): The swap number to query
Returns:
uint256: The configured minimum fill amount (0means no minimum)
isOpenOrder(uint256 swapNumber_) → bool
Returns whether a swap is an open order (one zero counterparty) that must be filled via takeOpenSell / takeOpenBuy.
Parameters:
swapNumber_(uint256): The swap number to query
Returns:
bool: True if the swap is an open order
maxSwapLifetime() → uint256
Returns the maximum allowed distance between block.timestamp and a swap's deadline at configure time, fixed at deployment.
Returns:
uint256: The maximum swap lifetime in seconds
MIN_SWAP_LIFETIME_LIMIT() → uint256
Returns the compile-time floor that maxSwapLifetime must not fall below at deployment.
Returns:
uint256: The minimum allowed value formaxSwapLifetime
MAX_SWAP_LIFETIME_LIMIT() → uint256
Returns the compile-time ceiling that maxSwapLifetime must not exceed at deployment.
Returns:
uint256: The maximum allowed value formaxSwapLifetime
requiredAllowance(address addr, address token) → uint256
Returns the required allowance for active swaps for a wallet and token.
Parameters:
addr(address): Wallet addresstoken(address): Token address
Returns:
uint256: Required allowance amount for the configurator
requiredAllowanceCounterparty(address addr, address token) → uint256
Returns the required allowance for active swaps as a counterparty.
Parameters:
addr(address): Wallet addresstoken(address): Token address
Returns:
uint256: Required allowance amount as counterparty (informational only)
pendingBuys(address addr) → uint256
Returns pending restricted tokens to buy for an address.
Parameters:
addr(address): Buyer address
Returns:
uint256: Amount of pending restricted tokens to buy
pendingSells(address addr) → uint256
Returns pending restricted tokens to sell for an address.
Parameters:
addr(address): Seller address
Returns:
uint256: Amount of pending restricted tokens to sell
restrictedLockupToken() → address
Returns the RestrictedLockupToken contract address.
Returns:
address: The RestrictedLockupToken contract address
accessControl() → address
Returns the AccessControl contract address.
Returns:
address: The AccessControl contract address
INTERFACE_ID() → bytes4
Returns the interface ID for ERC-165 support.
Returns:
bytes4: The interface identifier
isSwapExpired(uint256 swapNumber_) → bool
Checks if a specific swap has expired based on its deadline.
Parameters:
swapNumber_(uint256): The swap number to check
Returns:
bool: True if the swap has expired, false otherwise
Requirements:
- Swap record must exist
Note:
- A swap is considered expired if it has a deadline > 0 and the current block timestamp is greater than the deadline
- Configured swaps always carry a deadline > 0 (the
0sentinel is rejected at configure time)
Inherited Functions
Pausable Functions
paused() → bool
Returns whether the contract is currently paused.
Returns:
bool: True if paused, false otherwise
ERC-2771 Functions
isTrustedForwarder(address forwarder) → bool
Checks if an address is a trusted forwarder for meta-transactions.
Parameters:
forwarder(address): Address to check
Returns:
bool: True if the address is a trusted forwarder
trustedForwarder() → address
Returns the trusted forwarder address.
Returns:
address: The trusted forwarder address
ERC-165 Functions
supportsInterface(bytes4 interfaceId) → bool
Checks if the contract supports a specific interface.
Parameters:
interfaceId(bytes4): Interface identifier to check
Returns:
bool: True if the interface is supported
Events
SwapConfigured(uint256 indexed swapNumber, address indexed restrictedTokenSender, uint256 restrictedTokenAmount, address quoteToken, address indexed quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)
Emitted when a new swap is configured. An open sell order carries quoteTokenSender == address(0); an open buy order carries restrictedTokenSender == address(0).
Parameters:
swapNumber(uint256, indexed): Unique swap identifierrestrictedTokenSender(address, indexed): Address providing restricted tokens (address(0)for an open buy order)restrictedTokenAmount(uint256): Amount of restricted tokens in the swapquoteToken(address): Quote token contract addressquoteTokenSender(address, indexed): Address providing quote tokens (address(0)for an open sell order)quoteTokenAmount(uint256): Amount of quote tokens in the swapdeadline(uint256): Unix timestamp deadline for swap expirationminimumFillAmount(uint256): Minimum restricted-token amount a taker must fill on an open order (unless taking the entire remaining amount);0means no minimum
SwapComplete(uint256 indexed swapNumber, address indexed restrictedTokenSender, uint256 restrictedTokenAmount, address indexed quoteTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline)
Emitted when a swap is successfully completed.
Parameters:
swapNumber(uint256, indexed): Unique swap identifierrestrictedTokenSender(address, indexed): Address that provided restricted tokensrestrictedTokenAmount(uint256): Amount of restricted tokens swappedquoteTokenSender(address, indexed): Address that provided quote tokensquoteToken(address): Quote token contract addressquoteTokenAmount(uint256): Amount of quote tokens swappeddeadline(uint256): Unix timestamp deadline for swap expiration
SwapCanceled(address indexed sender, uint256 indexed swapNumber)
Emitted when a swap is canceled.
Parameters:
sender(address, indexed): Address that canceled the swapswapNumber(uint256, indexed): Unique swap identifier
OpenSwapFilled(uint256 indexed swapNumber, address indexed filler, uint256 fillRestrictedAmount, uint256 fillQuoteAmount, uint256 remainingRestrictedAmount)
Emitted on every partial or final fill of an open order via takeOpenSell / takeOpenBuy. A remainingRestrictedAmount of 0 indicates the order is now fully filled (a SwapComplete is emitted in the same transaction with the last filler as counterparty).
Parameters:
swapNumber(uint256, indexed): Unique swap identifierfiller(address, indexed): Address that took this fill (the actual counterparty for this slice)fillRestrictedAmount(uint256): Restricted-token amount transferred in this fillfillQuoteAmount(uint256): Quote-token amount transferred in this fillremainingRestrictedAmount(uint256): Restricted-token amount still unfilled after this event
OrderResized(uint256 indexed swapNumber, uint256 newRemainingRestrictedAmount, uint256 newRemainingQuoteAmount)
Emitted when an open order's remaining size changes without a fill — the owner called decreaseOrder / increaseOrder / increaseOrderWithPermit, or a linked settlement deducted from a parent offer (completeSwapWithRestrictedToken / takeOpenBuy with a sellSwapNumber). The size can both shrink and grow (possibly above the originally configured amount). If a shrink reaches zero, SwapCanceled is emitted in the same transaction and the order is terminal.
Parameters:
swapNumber(uint256, indexed): Unique swap identifier of the resized ordernewRemainingRestrictedAmount(uint256): Restricted-token amount still offered after the resizenewRemainingQuoteAmount(uint256): Quote-token amount still expected after the resize
Inherited Events
Paused(address account)
Emitted when the contract is paused.
Parameters:
account(address): Address that paused the contract
Unpaused(address account)
Emitted when the contract is unpaused.
Parameters:
account(address): Address that unpaused the contract
Custom Errors
Configuration Errors
RestrictedSwap_InvalidAccessControl()
Thrown when an invalid access control address is provided during construction.
RestrictedSwap_InvalidRestrictedLockupToken()
Thrown when a zero address is provided as the RestrictedLockupToken during construction.
RestrictedSwap_InvalidTrustedForwarder()
Thrown when a zero address is provided as the trusted forwarder during construction.
RestrictedSwap_InvalidMaxSwapLifetime()
Thrown when the constructor's maxSwapLifetime_ is outside [MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT].
RestrictedSwap_InvalidQuoteToken()
Thrown when a zero address is provided as the quote token.
RestrictedSwap_InvalidQuoteTokenSender()
Thrown when a zero address is provided as the quote token sender.
RestrictedSwap_InvalidRestrictedTokenSender()
Thrown when a zero address is provided as the restricted token sender.
RestrictedSwap_InvalidRestrictedTokenAmount()
Thrown when zero is provided as the restricted token amount.
RestrictedSwap_InvalidQuoteTokenAmount()
Thrown when zero is provided as the quote token amount.
Validation Errors
RestrictedSwap_QuoteTokenMustNotSupportIERC1404()
Thrown when the quote token supports ERC-1404 interface (restricted tokens not allowed as quote tokens).
RestrictedSwap_InsufficientRestrictedTokenAllowance()
Thrown when the restricted token allowance is insufficient for the swap.
RestrictedSwap_InsufficientQuoteTokenAllowance()
Thrown when the quote token allowance is insufficient for the swap.
RestrictedSwap_InsufficientRestrictedTokenAmount()
Thrown when the restricted token balance is insufficient for the swap.
RestrictedSwap_InsufficientQuoteTokenAmount()
Thrown when the quote token balance is insufficient for the swap.
RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit()
Thrown by a ...WithPermit function when the restricted-token allowance is still insufficient after applying the permit signature (e.g. permitValue was under-sized for the requirement at execution time, or the signature was invalid).
RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit()
Thrown by a ...WithPermit function when the quote-token allowance is still insufficient after applying the permit signature (e.g. permitValue was under-sized for the requirement at execution time, or the signature was invalid).
RestrictedSwap_DeadlineRequired()
Thrown at configure time when deadline is 0 (the "no deadline" sentinel is no longer accepted).
RestrictedSwap_DeadlineExceedsMaxLifetime()
Thrown at configure time when deadline - block.timestamp exceeds the contract's maxSwapLifetime.
Swap State Errors
RestrictedSwap_AlreadyCanceled()
Thrown when attempting to operate on a swap that has already been canceled.
RestrictedSwap_AlreadyCompleted()
Thrown when attempting to operate on a swap that has already been completed.
RestrictedSwap_InvalidSwapStatus()
Thrown when the swap status doesn't match the required status for the operation.
RestrictedSwap_InvalidSwapRecord()
Thrown when attempting to query a swap that doesn't exist.
RestrictedSwap_SwapExpired()
Thrown when attempting to complete a swap that has expired based on its deadline.
RestrictedSwap_SwapNotConfigured()
Thrown when attempting to cancel a swap that hasn't been properly configured.
Authorization Errors
RestrictedSwap_InvalidTokenSender()
Thrown when the caller is not authorized to complete the swap (not the expected token sender).
RestrictedSwap_InvalidCanceler()
Thrown when the caller is not authorized to cancel the swap via cancelSwap (only the owner may cancel a live order; anyone may cancel an expired one). Owner checks on the resize paths use RestrictedSwap_InvalidOrderOwner instead.
RestrictedSwap_InvalidOrderOwner()
Thrown when decreaseOrder / increaseOrder is called by someone other than the order's creator.
Open Order & Resize Errors
RestrictedSwap_NotOpenOrder()
Thrown when an open-order-only operation (takeOpenSell / takeOpenBuy / decreaseOrder / increaseOrder) targets a closed swap (both counterparties named).
RestrictedSwap_OpenOrderRequiresTake()
Thrown when completeSwapWithQuoteToken / completeSwapWithRestrictedToken (or their permit variants) target an open order, which must instead be filled via takeOpenSell / takeOpenBuy.
RestrictedSwap_SelfFillNotAllowed()
Thrown when the configurer of an open order attempts to fill it themselves (takeOpenSell by the order's restrictedTokenSender, or takeOpenBuy by its quoteTokenSender).
RestrictedSwap_InvalidFillAmount()
Thrown when a takeOpenSell / takeOpenBuy fill amount is zero or exceeds the order's remaining size.
RestrictedSwap_FillBelowMinimum()
Thrown when a take fill is below the order's minimumFillAmount and does not take the entire remaining amount (a below-minimum tail can always be taken in full).
RestrictedSwap_InvalidMinimumFillAmount()
Thrown at configure time when minimumFillAmount exceeds restrictedTokenAmount, or is non-zero on a closed swap (both counterparties named — closed swaps settle in full and never consult the minimum).
RestrictedSwap_AmountNotDivisible()
Thrown when a fill, resize, or linked parent deduction amount does not produce an integer quote amount at the order's original price ratio (strict exact-pricing rule).
RestrictedSwap_InvalidResizeAmount()
Thrown when decreaseOrder is given a size that is not strictly smaller, or increaseOrder a size that is not strictly larger, than the order's current remaining amount.
RestrictedSwap_ExceedsParentRemaining()
Thrown when a linked settle or fill (sellSwapNumber named) would deduct more than the parent offer's remaining size — e.g. a third party filled the parent between signing and execution. Callers should retry with sellSwapNumber = 0 and resize the listing separately.
Transfer Errors
RestrictedSwap_InconsistentQuoteTokenAmount()
Thrown when the actual quote token transfer amount doesn't match the expected amount.
RestrictedSwap_InconsistentRestrictedTokenAmount()
Thrown when the actual restricted token transfer amount doesn't match the expected amount.
Inherited Errors
EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(address addr)
Thrown when an address lacks both CONTRACT_ADMIN_ROLE and TRANSFER_ADMIN_ROLE.
Parameters:
addr(address): The unauthorized address
EnforcedPause()
Thrown when an operation is attempted while the contract is paused.
ExpectedPause()
Thrown when attempting to pause an already paused contract or unpause an unpaused contract.
ReentrancyGuardReentrantCall()
Thrown when a reentrant call is detected.
SafeERC20FailedOperation(address token)
Thrown when an ERC-20 operation fails.
Parameters:
token(address): The token contract address where the operation failed