Changelog
Current contract suite: 5.1.3 (RestrictedLockupToken.contractVersion), RestrictedSwap 1.1.0.
5.1.1 → 5.1.3
Deployment bytecode changed for every contract in the suite except AccessControl, which is
byte-for-byte identical. Anyone re-deploying must use the refreshed artifacts; contracts already
on chain are unaffected but must be verified against the matching source bundle.
Breaking ABI changes
PurchaseContract — cancellations are now keyed by purchase ID, so a cancelled ID cannot be replayed or subsidised by another purchase:
| Removed | Replaced by |
|---|---|
cancelPurchase(address paymentTokenAddress_, uint256 amount_) | cancelPurchase(string purchaseId_, address paymentTokenAddress_, uint256 amount_) |
event PurchaseCanceled(address, address, address, uint256) | event PurchaseCanceled(string purchaseId, address authorityAddress, address paymentTokenAddress, address canceledPurchaseWallet, uint256 amount) |
New reads and errors: isPurchaseIdCanceled(string), canceledPurchaseIds(string),
PurchaseContract_InvalidPurchaseId(), PurchaseContract_PurchaseIdCanceled(). A non-zero payment
amount is now required, so a purchase can no longer mint unbacked tokens.
RestrictedSwap — all seven *WithPermit entry points take the permit value explicitly instead of
recomputing it from mutable reservation state, which previously made an approve+action pair
single-use. Each gains uint256 permitValue immediately before permitDeadline:
completeSwapWithQuoteTokenPermitcompleteSwapWithRestrictedTokenPermitconfigureBuyWithPermitconfigureSellWithPermitincreaseOrderWithPermittakeOpenBuyWithPermittakeOpenSellWithPermit
Payer-side quote-token debit checks were added, so overpayment is rejected rather than absorbed.
Additive ABI changes
RestrictedLockupToken (and mirrored across all three extensions)
registryUnordered() → boolandevent GlobalRegistryBecameUnordered(uint256, uint256)— the global holdings registry no longer relies on a zero sentinel, so reclassification of holdings can no longer happen silentlypreviewTransferableFromHoldings(address, address, uint256) → uint256(management extension) — per-holding eligibility scanning now takes a recipient-specific cursor- New errors
RestrictedLockupToken_TransfersPaused()andRestrictedLockupToken_ReclaimToFrozen(address);cancelTimelocknow enforces the basic transfer restrictions, so tokens cannot move while paused, frozen, or AML-invalid - Zero-amount ERC-20 transfers no longer create a holder record
InterestPayment
- Principal redemption is staged and inspectable:
requiredPrincipalFunding() → uint256,principalRedemptionOpen() → bool,principalAmountPerWholeToken() → uint256,event PrincipalFullyFunded(uint256) - New errors
InterestPayment_PrincipalNotFullyFunded(uint256, uint256),InterestPayment_PrincipalFundingExceedsEntitlement(uint256, uint256),InterestPayment_IncompatibleTokenDecimals(uint8, uint8),InterestPayment_InvalidRecipientAddress() - Per-period accumulators widened past
uint128, per-period caps no longer read a stale global pool, partial funding no longer denies principal redemption, and reclaim/force-claim paths reject the contract's own address so funds cannot be stranded principalAmountPerTokenunit mismatch fixed, with an exact-funding invariant and a constructor decimals guard- Outbound ERC-20 fee/rebase amounts are now handled, so transfers cannot silently underpay
TransferRules
MAX_LOCK_DURATION() → uint256,NEVER_UNLOCKS() → uint256, andeligibilityThresholdFor(uint256, (uint256[], uint256, uint256, uint256, bool), bool) → (bool, uint256, uint256)- New error
TransferRules_LockDurationTooLarge(): out-of-range lock durations are rejected at pack time rather than wrapping getUnlockTimestampno longer returns a misleading value, and token-type determination is no longer order-dependent when Identity Registry regions change- A region value of
0is an exact match, not a wildcard
ERC2771CustomForwarder
- New error
ERC2771CustomForwarder_NonZeroValueNotAllowed()— native value is rejected executeBatchin atomic mode handles empty reverts, so a batch can no longer burn relayer ETH or silently execute non-atomically; requests with already-consumed nonces are skipped in non-atomic batches
Behavioural fixes with no ABI change
- IdentityRegistry — bytecode changed, ABI identical
- SnapshotPeriods — unbounded
amount × timearithmetic no longer overflows under extreme supply - RecallablePayment — outbound ERC-20 fee/rebase handling, matching InterestPayment
Unchanged
- AccessControl — identical ABI and identical bytecode
v5.0 → v5.1 (suite 5.1.1)
RestrictedLockupToken
- New
RestrictedLockupTokenStandardsExtension(delegatecall) exposing EIP-2612permit,nonces, andDOMAIN_SEPARATOR - Main token constructor adds a
restrictedLockupTokenStandardsExtensionaddress argument - Deploy order inserts StandardsExtension immediately before the token: Extension → ManagementExtension → StandardsExtension → Token
InterestPayment
- Interest rate bips precision raised: 100 bips = 1% (v5) → 100,000 bips = 1% (v5.1) (
BIPS_PRECISION = 10_000_000for 100%) - Example: a 5% rate was
500bips in v5 and is500_000bips in v5.1 - Constructor argument list is unchanged; only the scale of rate values changes
RestrictedSwap
RestrictedSwap contract version 1.1.0 — largest surface change in v5.1.
- Constructor gains immutable
maxSwapLifetime(seconds; allowed range[1 hour, 365 days]) - Swap deadlines are required and must satisfy
deadline - block.timestamp <= maxSwapLifetime - Open orders (
address(0)counterparty) with partial fills viatakeOpenBuy/takeOpenSell(and permit variants) configureBuy/configureSellgainminimumFillAmount(must be0for closed swaps)- Order resize without cancel/recreate:
decreaseOrder,increaseOrder,increaseOrderWithPermit - Parent-offer linking:
completeSwapWithRestrictedTokentakes an optionalsellSwapNumber_to deduct from an open sell - EIP-2612 permit variants across configure, complete, take-open, and increase paths
- New events and errors for open orders, deadlines, resize, and self-fill prohibition