BondRegistry Documentation
Overview
The BondRegistry contract is a central registry for managing bonds in the DEUSS system. It handles the lifecycle of bonds from publishing to issuance and redemption, implementing a role-based access control system for different operations.
The registry uses a bond + tranche model: one Bond holds the shared terms and a single ERC-6909 token identity, while each call to issueBond creates one Tranche record tracking when and how much was issued. This allows a bond to be issued in multiple tranches under the same ISIN and the same token ID.
Current issuance accounting separates cumulative historical issuance from future headroom. None of these registry fields track live/circulating supply — that is the token contract's totalSupply(tokenId). Burns reduce live supply but never reduce mintedSupply:
mintedSupply: cumulative amount ever issued across all tranches, monotonically increasing, never reduced by burnsmaxSupply: maximum live (concurrently outstanding) supply capacity. Issuer-reclaim burns restoreremainingIssuableSupply, so cumulativemintedSupplymay exceedmaxSupplyover the bond's life — it caps reissuable headroom, not the total amount ever issuedremainingIssuableSupply: remaining issuance headroom againstmaxSupply, decreased on issue and restored on issuer-reclaim burnissuanceClosed: permanent flag that blocks future tranches without affecting live supplyisGuaranteed: immutable on-chain metadata showing whether that exact bond version is backed by a guaranteeissuanceCountry: numeric ISO 3166 country code recorded for the exact bond version
The registry also stores append-only issuer scoring records by wallet. Scoring records are independent of bond versions and are addressed by a 1-based wallet-local scoring ID.
Role assignments for registry operations are summarized in roles.md. Governance/timelock execution is documented in TimelockController.md.
Prerequisites
- Contract must be properly initialized with an owner
- Bond-specific roles must be assigned for administrative functions
- Token contract must be set via
setMultiToken
Contract Architecture
The BondRegistry is a core component of the bond management system:
- Inherits from
OwnableRolesExtensionfor role-based access control - Manages bond data with version-aware storage (
_bonds,_tranches,_isinToLatestVersion,_isinToActiveVersion) - Uses a single shared ERC-6909 token contract for all bonds, with unique token IDs per
(isin, version) - Each bond's token ID is computed from
keccak256(abi.encodePacked(isin, version)) - Uses
ReentrancyGuardto protect against reentrancy attacks ISINidentifies the bond family, whileversionidentifies the concrete bond series under that ISINactiveVersionis the issuer-designated current version returned by ISIN-only read helpers- Older versions can remain live in
Replacedwhile a newer version is already active
Allowed Bond Status
Below is a list of statuses for a bond:
- Unregistered: Default state - bond does not exist in the registry.
- Published: Bond version is published on-chain but not yet issued. This is the draft phase.
- Issued: Bond version is active and has issued supply.
- Suspended: The bond version has been administratively suspended and cannot be replaced.
- Redeemed: Reserved for principal redemption semantics.
- Cancelled: Unissued bond version cancelled before any tranche was created.
- Replaced: Deprecated legacy version. It still exists and may still be used for migration / legacy handling.
Bond Lifecycle Flow
Unregistered → Published → Issued ⇄ Issued (subsequent tranches)
↓ ↓
Cancelled Suspended ↔ Issued
↓
Redeemed
Issued active version → Replaced (when successor version is first issued)
Replaced → FINAL_SETTLEMENT burns and issuer recovery (no closeBond transition)
Expected Workflows
The registry tracks each ISIN as a versioned bond family. latestVersion is the highest version ever published for the ISIN, including cancelled drafts, while activeVersion is the current series used by ISIN-only read helpers. Versions and their token IDs are monotonic: cancelling an unpublished draft does not make its version or token ID reusable.
| Current ISIN state | Allowed workflow | Result |
|---|---|---|
No version exists (latestVersion == 0, activeVersion == 0) | Call publishBond | Creates version 1; latestVersion and activeVersion become 1 |
Latest version is Published and unissued | Call updatePublishedBond to amend the draft | Same version and token ID stay in place; activeVersion is unchanged |
First version is Published and unissued, but should be discarded | Call cancelBond(isin, 1), then call publishBond again if needed | Cancellation keeps latestVersion at 1 and clears activeVersion; the next publish creates version 2 with a fresh token ID |
Active version is Issued and no successor is pending | Call publishBond | Creates the next successor version in Published status; the issued predecessor remains active |
Active version is Issued and latest successor is Published but unissued | Call updatePublishedBond to amend the successor, issueBond to activate it, or cancelBond on that latest successor before closing the active predecessor or publishing a replacement draft | Cancelling the successor preserves it as cancelled history; the next publish creates a later successor version with a fresh token ID. The active predecessor cannot be closed while the successor is pending |
Successor version is Published and ready for sale | Call issueBond for the successor | First successor issuance switches activeVersion to the successor and marks the previous active issued version as Replaced |
Active version is Suspended | Call unsuspendBond before publishing a successor | publishBond reverts while the active version is not Issued |
Active version is Redeemed, Cancelled, or another non-Issued status | No successor publish workflow is available through publishBond | publishBond reverts because the active version is not issuable |
Historical non-latest version is Published | Do not cancel it through the normal workflow | cancelBond only accepts the current latest version |
For a same-ISIN republish, first discard the current unissued draft with cancelBond; otherwise publishBond reverts because a pending Published version already exists. Issued versions are not republished in place: a later publishBond creates a successor, and the active pointer changes only when that successor is first issued.
Replacement and Migration Model
Replaced is reserved for rare lifecycle events where a bond family keeps the same ISIN but a new version becomes the active series. The first issuance of a successor version marks the previously active issued version as Replaced and switches activeVersion to the successor.
The registry does not perform holder migration by itself. Any holder migration must be handled outside this registry, for example through a dedicated migration contract, operator-assisted exchange, or another controlled process. Until such a process is executed, the replaced version remains an existing token series.
FINAL_SETTLEMENT burns and rotateIssuer recovery are allowed for both Issued and Replaced versions so legacy supply can be retired during late settlement or migration cleanup while issuer metadata remains recoverable.
Replaced versions are not closeable through closeBond; that function currently accepts only Issued and Suspended versions with zero token supply. Replaced legacy supply can be retired through FINAL_SETTLEMENT burns, but the bond version remains in Replaced status.
Authorization Requirements
- BondRegistry uses granular roles:
PUBLISHERforpublishBondandupdatePublishedBondCANCELforcancelBondCLOSEforcloseBondCURRENCYforsetAllowedCurrencySUSPENDforsuspendBondUNSUSPENDforunsuspendBondBURNERfor third-party final-settlement burns routed throughburnBond/burnBondBatchSCORINGfor appending issuer scoring recordsISSUER_RECOVERYfor rotating a bond issuer during exceptional recovery; this is a critical governance-only role and should be granted only toTimelockController
issueBondcan be called by an address withPUBLISHERrole or bybond.issuer; when called asbond.issuer, the issuer account must be enabled inEntityRegistrycloseIssuancecan be called bybond.issueror an address withPUBLISHER; when called asbond.issuer, the issuer account must be enabled inEntityRegistry- For
issueBondandcloseIssuance, issuer identity takes precedence over role-based authority: ifmsg.sender == bond.issuer, the call follows the issuer path and requires an enabled issuer account even when the same address also holdsPUBLISHER rotateIssuercan be called only byISSUER_RECOVERYforPublished,Issued,Suspended, andReplacedversions; production use should execute through timelock governance, and the replacement issuer must be enabled inEntityRegistryburnBond/burnBondBatchuse explicit burn semantics:ISSUER_RECLAIM: caller must bebond.issuer, the issuer account must be enabled inEntityRegistry,from == bond.issuer, the bond must beIssued, and the reclaimed amount must not exceed the issuer's unfrozen token balanceFINAL_SETTLEMENT: bond must beIssuedorReplaced; issuer identity path requires enabled issuer and may only burn their own balance; role-based path requiresBURNERand an enabled caller account
- Actor-identity authority (
bond.issuer) is only valid while the actor account is enabled; third-partyBURNERauthority also requires an enabled caller account - Only the owner can:
- Set token contract (
setMultiToken) - Grant/revoke roles
- Set token contract (
Parameters That Can Be Updated
Standard amendment path (updatePublishedBond)
The following fields can be amended through updatePublishedBond only while the exact bond version is in Published status. In the normal lifecycle, a Published version is unissued and has trancheCount == 0:
issuer- Bond issuer addresscurrency- Currency code; changing it requires the new currency to be allowed, while keeping the stored currency is permitted even if it was disabled after publicationbondNominalValue- Face value metadata per bond unit, using the caller/integrator-defined scalemaxSupply- Hard cap on total issuable token unitscouponRateType- Type of coupon (fixed, floating, zero coupon)couponRates- Interest rate checkpoints, expressed in basis pointscouponFrequency- Payment frequencymaturityDate- Maturity dateissuanceCountry- Numeric ISO 3166 country code of issuance
Emergency recovery path (rotateIssuer)
issuercan also be rotated forPublished,Issued,Suspended, orReplacedversions through theISSUER_RECOVERYrole- Production issuer recovery should execute through timelock governance and requires a non-zero reason
- The replacement issuer must be non-zero, different from the stored issuer, and enabled in
EntityRegistry rotateIssuerupdates only the stored issuer address; it does not change financial terms, token ID, tranche records,mintedSupply,remainingIssuableSupply,latestVersion, oractiveVersionCancelledandRedeemedversions are not issuer-rotatable
❌ Never Updatable:
isin- Bond identifier (immutable)isGuaranteed- Guarantee-backed metadata for the bond version
Special Restrictions
For Published Bonds:
- Standard amendments through
updatePublishedBondare available only while inPublishedstatus - In the normal lifecycle, a
Publishedversion hastrancheCount == 0; issuance moves it toIssued updatePublishedBondamends the exact unpublished version in place- A successor version is created only by a fresh
publishBondcall for the same ISIN
For Issued Bonds:
- Cannot be amended through
updatePublishedBond - Can be suspended, have issuance closed, or later be marked
Redeemed - If a newer successor version is issued, the previously active issued version becomes
Replaced - May have
issuerrotated only through the exceptionalISSUER_RECOVERYpath while the version remainsIssued,Suspended, orReplaced
For All Bonds:
- ISIN must be exactly 12 bytes with uppercase letters in positions
0..1, uppercase letters or digits in positions2..10, and a digit in position11 - Currency must be in the allowed list when publishing a bond or changing a published bond's currency
- Currency strings must be exactly 3 uppercase ASCII letters before conversion to
bytes3 - String-based ISIN and currency inputs are converted through
StringExtensionsbefore storage or allowlist lookup; malformed strings revert before lookup logic runs - Bond nominal value and max supply must be greater than zero
bondNominalValuehas no registry-enforced decimal precision; publishers and integrators must agree on the unit scale for each bond- Issuance country must be non-zero
Coupon Types
ZERO_COUPON- The coupon does not bear any interestFIXED- Fixed interest between two explicit timestamp checkpoints.FLOATING- Predefined dynamic interest where rates change at explicit timestamp checkpoints.
Coupon Rates
CouponRates struct contains 2 arrays: paymentTimestamps and rates. All non-zero rates values are expressed in basis points: 1 = 0.01%, 100 = 1%, and 500 = 5%.
paymentTimestamps are absolute Unix timestamps. Each timestamp is an inclusive lower bound for the rate at the same array index. The final checkpoint must carry rate 0 and marks the end of coupon-bearing time. couponFrequency remains descriptive metadata for display and categorization; coupon-rate resolution does not derive dates from it.
ZERO_COUPON
For ZERO_COUPON type, both arrays must be empty, as there is no interest on the coupon.
FIXED
FIXED type is represented by exactly 2 items in each array:
paymentTimestamps- inclusive start timestamp for the fixed rate, followed by the timestamp where coupon rate becomes 0rates- rate at the left bound in basis points, and 0 at the right bound
Example of couponRates for FIXED coupon type. Assuming the coupon-bearing period starts at timestamp 1_735_689_600 and ends at timestamp 1_767_225_600, the period bears 5% (or 500 bp) interest until the terminal zero-rate checkpoint.
CouponRates({
paymentTimestamps: [1_735_689_600, 1_767_225_600],
rates: [500, 0]
})
FLOATING
FLOATING type extends on the FIXED, allowing for more rates over time (minimum 2 elements). Each non-terminal rate is expressed in basis points.
Example: 5% from timestamp 1_735_689_600, then 4% from 1_767_225_600, then 3% from 1_783_036_800, and finally 0 interest from 1_790_812_800.
CouponRates({
paymentTimestamps: [1_735_689_600, 1_767_225_600, 1_783_036_800, 1_790_812_800],
rates: [500, 400, 300, 0]
})
Coupon Rate Validation Rules
paymentTimestampsandratesarrays must have the same length- For
ZERO_COUPON: both arrays must be empty - For
FIXED: exactly 2 elements required - For
FLOATING: at least 2 elements required - First payment timestamp cannot be 0
- Last rate must be 0 (end marker)
- Terminal payment timestamp cannot be after maturity
- Payment timestamps must be in ascending order
- No duplicate adjacent rates allowed
Core Functions
publishBond(BondInput calldata bondInput)
Publishes a new bond version to the registry.
Prerequisites:
- Caller must have
PUBLISHER - ISIN must satisfy the exact 12-byte
StringExtensionsrule:A-Zin positions0..1,A-Zor0-9in positions2..10, and0-9in position11 - For a new ISIN: version
1is created and becomesactiveVersion - For an existing ISIN with cancelled draft history and no active version: the next monotonic version is created and becomes
activeVersion - For an existing ISIN with an active version: current
activeVersionmust beIssued - For an existing ISIN: there must not already be a pending successor version in
Published - BondInput data must be valid
- Currency must be exactly 3 uppercase ASCII letters and must be allowed
Parameters:
bondInput: BondInput struct containing:isin: Bond identifier (12-byte ISIN-style string with positional uppercase alphanumeric rules; no checksum validation)issuer: Address of the bond issuercurrency: Currency code (3 uppercase ISO 4217-style letters)bondNominalValue: Face value metadata per token unit, using the caller/integrator-defined scalemaxSupply: Hard cap on total issuable token unitscouponRateType: Type of couponcouponRates: Coupon rate checkpoints, with rates expressed in basis pointscouponFrequency: Payment frequencymaturityDate: Maturity dateisGuaranteed: Whether this bond version is backed by a guaranteeissuanceCountry: Numeric ISO 3166 country code of issuance
Events:
BondPublished(bytes12 isin, address issuer, uint8 version, uint256 tokenId, address tokenAddress, bytes3 currency, uint256 bondNominalValue, uint256 maxSupply, uint256 maturityDate, CouponFrequency couponFrequency, CouponRateType couponRateType, bool isGuaranteed, uint16 issuanceCountry, address publisher): Publication snapshot for offchain indexers
Errors:
BondRegistry__ActiveVersionNotIssuable(bytes12): When publishing a successor while the current active version is notIssuedBondRegistry__SuccessorAlreadyPublished(bytes12): When a pending successor version already exists inPublishedstatusBondRegistry__InvalidCurrency(): When currency is not allowedBondRegistry__IssuerNotEnabled(address): When issuer is not an enabled account in EntityRegistryBondRegistry__BondNominalValueIsZero(): When bondNominalValue is zeroBondRegistry__MaxSupplyIsZero(): When max supply is zeroBondRegistry__MaturityDateExpired(): When maturity date is not strictly in the futureBondRegistry__IssuanceCountryIsZero(): When issuance country is zeroZeroAddress(): When issuer address is zeroBondRegistry__CouponRatesLengthMismatch(): When arrays have different lengthsBondRegistry__CouponRatesUnexpectedLength(): When array length doesn't match coupon typeBondRegistry__PaymentTimestampZero(): When first timestamp is 0BondRegistry__CouponRatesLastRateNotZero(): When last rate is not 0BondRegistry__PaymentTimestampAfterMaturity(): When the terminal timestamp is after maturityBondRegistry__PaymentTimestampsUnordered(): When timestamps are not strictly ascending (equal consecutive timestamps are rejected)BondRegistry__CouponRatesDuplicate(): When adjacent coupon rates are duplicatedStringExtensions__InvalidBytesLength(): When ISIN or currency byte length is invalidStringExtensions__NonAsciiCharacter(): When ISIN or currency contains a byte outside 7-bit ASCIIStringExtensions__InvalidCharacter(): When ISIN or currency contains control characters, lowercase letters, punctuation, letters/digits in disallowed positions, or other ASCII bytes outside the allowed charset
Sequence Diagram:
updatePublishedBond(BondInput calldata bondInput, uint8 version)
Amends an already-created unpublished bond version in place.
Prerequisites:
- Caller must have
PUBLISHER - Target version must be in
Publishedstatus - BondInput data must be valid
- If
bondInput.currencydiffers from the stored currency, the new currency must be allowed bondInput.isGuaranteedmust match the value stored when that version was published
Parameters:
bondInput: Same aspublishBondversion: Exact unpublished version to amend
Events:
PublishedBondUpdated(bytes12 isin, uint8 version, address publisher, address issuer, uint256 tokenId, address tokenAddress, bytes3 currency, uint256 bondNominalValue, uint256 maxSupply, uint256 maturityDate, CouponFrequency couponFrequency, CouponRateType couponRateType, bool isGuaranteed, uint16 issuanceCountry): Updated bond snapshot
Errors:
BondRegistry__InvalidBondStatus(bytes12): When the target version is not inPublishedstatus, including unregistered or already-issued versionsBondRegistry__GuaranteeStatusImmutable(bytes12): When attempting to changeisGuaranteed- Same validation errors as
publishBond
Notes:
- This does not create a successor version
- This does not change
activeVersion - Removing a currency from the allowlist does not block amendments that keep a published bond's existing currency unchanged
- This is only for amending a
Publisheddraft phase version
Sequence Diagram:
cancelBond(string calldata isin, uint8 version)
Cancels a published bond that has not yet been issued.
Prerequisites:
- Caller must have
CANCEL - Target version must be in
Publishedstatus - Target version must have
trancheCount == 0 - Target version must be the current latest version for the ISIN
Parameters:
isin: Bond identifierversion: Exact unpublished version to cancel
Notes:
- Cancelling the first unissued version keeps
latestVersionat the cancelled version and clearsactiveVersion, so the ISIN can be published again only as a later version with a fresh token ID. - Cancelling an unpublished successor preserves
latestVersionand leaves the previous issued active version unchanged. - Cancelled draft versions are never reused, preventing token ID scoped approvals from carrying into a republished replacement bond.
Events:
BondCancelled(bytes12 indexed isin, uint8 indexed version, address indexed executor): Cancellation status transition
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidBondStatus(bytes12): When bond is not inPublishedstatus or is not the latest versionBondRegistry__BondAlreadyIssued(bytes12): When bond already has tranches (trancheCount > 0)
Sequence Diagram:
issueBond(string calldata isin, uint8 version, uint256 amount)
Issues one tranche of an exact bond version by minting tokens to the issuer. May be called multiple times on the same version, each call creating a new tranche record.
Prerequisites:
- Caller must have
PUBLISHERrole or bebond.issuer; if caller isbond.issuer, the issuer account must be enabled inEntityRegistry - Target version must be in
PublishedorIssuedstatus issuanceClosedmust befalseamountmust be greater than zeroblock.timestampmust be beforebond.maturityDateamountmust not exceedbond.remainingIssuableSupply
Parameters:
isin: Bond identifierversion: Exact version to issueamount: Number of tokens to mint in this tranche
Events:
BondIssued(bytes12 indexed isin, uint256 indexed tokenId, uint256 indexed amount, uint16 trancheId, uint8 version, address caller): Emitted when a tranche is issued
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidBondStatus(bytes12): When bond is not inPublishedorIssuedstatusBondRegistry__UnauthorizedIssuer(address): When caller is neither PUBLISHER nor bond.issuerBondRegistry__IssuerNotEnabled(address): When caller is bond.issuer but the issuer account is disabled in EntityRegistryBondRegistry__IssuanceClosed(bytes12): When future issuance was permanently closedBondRegistry__IssuanceAmountIsZero(): Whenamount == 0BondRegistry__MaturityDateExpired(): When called after maturity dateBondRegistry__MaxSupplyExceeded(uint256 amount, uint256 remaining): When issuance would exceedmaxSupply
Notes:
- Each call creates exactly one tranche record with
trancheId = trancheCount + 1 - All tranches share the same
tokenAddressandtokenId Bond.mintedSupplyis updated cumulatively across all tranches and never decreasesBond.remainingIssuableSupplydecreases on issuance and is the operative future-cap check- First issuance transitions the target version from
PublishedtoIssued - If the issued version is a newer successor, the previous
activeVersionis markedReplacedandactiveVersionswitches to the newly-issued version
Sequence Diagram:
closeIssuance(string calldata isin, uint8 version)
Permanently disables future tranches for a bond without affecting already-issued live supply.
Prerequisites:
- Caller must be
bond.issueror havePUBLISHER; if caller isbond.issuer, the issuer account must be enabled inEntityRegistry - Target version must be in
IssuedorSuspendedstatus; unissuedPublishedversions should be cancelled instead issuanceClosedmust currently befalse
Events:
BondIssuanceClosed(bytes12 indexed isin, uint8 indexed version, address indexed caller)
Errors:
BondRegistry__InvalidBondStatus(bytes12): When bond is not in a closeable issuance stateBondRegistry__UnauthorizedIssuanceCloser(address): When caller is neither issuer nor publisherBondRegistry__IssuerNotEnabled(address): When caller is bond.issuer but the issuer account is disabled in EntityRegistryBondRegistry__IssuanceClosed(bytes12): When issuance is already closed
rotateIssuer(string calldata isin, uint8 version, address newIssuer, bytes32 reason)
Rotates the stored issuer for an exact bond version during exceptional recovery, for example when the current issuer wallet is disabled, compromised, or operationally unavailable. This updates registry metadata only, already-minted balances and marketplace positions remain separate recovery surfaces handled by token force-transfer, burn, freeze, or seizure workflows.
Prerequisites:
- Caller must have
ISSUER_RECOVERY; in production this role should be held only byTimelockController - Target version must exist and be in
Published,Issued,Suspended, orReplacedstatus newIssuermust be non-zero, different from the current issuer, and enabled inEntityRegistryreasonmust be non-zero- The current issuer is not required to be disabled on-chain, timelock governance is responsible for validating the recovery case before execution
Parameters:
isin: Bond ISINversion: Exact bond version whose issuer is rotatednewIssuer: Enabled replacement issuer walletreason: Operator reason code recorded in the event
Events:
BondIssuerRotated(bytes12 indexed isin, uint8 version, address indexed oldIssuer, address indexed newIssuer, bytes32 reason, address caller)
Errors:
BondRegistry__NonExistentBond(bytes12): When the exact version does not existBondRegistry__InvalidBondStatus(bytes12): When the bond isCancelled,Redeemed, or otherwise not recoverableZeroAddress(): WhennewIssueris zeroBondRegistry__IssuerUnchanged(address): WhennewIssueris already the stored issuerBondRegistry__ZeroReason(): Whenreasonis zeroBondRegistry__IssuerNotEnabled(address): WhennewIssueris not enabled inEntityRegistry
Sequence Diagram:
burnBond(bytes12 isin, uint8 version, address from, uint256 amount, BurnKind kind)
Burns supply for an exact bond version through the registry and applies the matching bond-level accounting.
Prerequisites:
- For
ISSUER_RECLAIM:- target version must be
Issued - caller must equal
bond.issuer - issuer account must be enabled in
EntityRegistry frommust equalbond.issueramountmust not exceed the issuer's unfrozen balance for this token ID
- target version must be
- For
FINAL_SETTLEMENT:- target version must be
IssuedorReplaced - issuer identity path: caller equals
bond.issuer, issuer must be enabled inEntityRegistry, andfrommust equalbond.issuer - role-based path: caller must have
BURNERand be enabled inEntityRegistry
- target version must be
Events:
BondBurned(bytes12 indexed isin, uint8 version, address indexed from, uint256 indexed amount, BurnKind kind, address caller)
Errors:
BondRegistry__InvalidBondStatus(bytes12): When bond is not burnableBondRegistry__UnauthorizedBurnCaller(address, uint8): When caller is not authorized for the requested burn kindBondRegistry__IssuerNotEnabled(address): When caller is bond.issuer but the issuer account is disabled in EntityRegistryBondRegistry__InvalidBurnSource(address, bytes12, uint8): Whenfromis invalid for the requested burn kindBondRegistry__FrozenIssuerReclaimDenied(address, bytes12, uint8, uint256, uint256): WhenISSUER_RECLAIMwould consume frozen issuer-held inventory
Important Notes:
ISSUER_RECLAIMis limited to unfrozen issuer-held balance of the exact version and increasesremainingIssuableSupplyISSUER_RECLAIMdoes not consume frozen balances; freezer operators must explicitly unfreeze inventory before it can be reclaimed and reissuedFINAL_SETTLEMENTis allowed for bothIssuedandReplacedlegacy versionsFINAL_SETTLEMENTreduces live token supply only and does not restore issuance headroomFINAL_SETTLEMENTdoes not requireBURNERwhen an enabled issuer burns only their own balance- Third-party
BURNERcallers must remain enabled inEntityRegistry; disabling the account blocks settlement burns until it is re-enabled mintedSupplyremains cumulative issuance history for both burn kinds
closeBond(string calldata isin, uint8 version)
Closes an exact bond version by marking it as Redeemed. This entrypoint is reserved for principal redemption flow semantics and requires that all tokens have already been burned (totalSupply == 0).
Prerequisites:
- Caller must have
CLOSE - Target version must be in a closeable status
totalSupply(bond.tokenId)must be zero- If the target is the active version, no later successor may still be pending in
Publishedstatus
Parameters:
isin: Bond identifierversion: Exact version to close
Events:
BondRedeemed(bytes12 indexed isin, uint8 indexed version, address indexed executor): Redemption status transition
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidBondStatus(bytes12): When bond is not inIssuedorSuspendedstatusBondRegistry__TotalSupplyNotZero(): When token total supply is not zeroBondRegistry__SuccessorAlreadyPublished(bytes12): When closing the active version while a later successor is still pending inPublished
Sequence Diagram:
suspendBond(string calldata isin, uint8 version)
Suspends an issued bond and pauses its token ID.
Prerequisites:
- Caller must have
SUSPEND - Bond must be in
Issuedstatus
suspendBond can be executed while the token contract is globally paused. The underlying token-id pause is an additional restriction and remains available during emergency lockdown.
Parameters:
isin: Bond identifierversion: Exact version to suspend
Events:
BondSuspended(bytes12 indexed isin, uint8 indexed version, address indexed executor): Suspension status transition
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidBondStatus(bytes12): When bond is not inIssuedstatus
Sequence Diagram:
unsuspendBond(string calldata isin, uint8 version)
Unsuspends a suspended bond and unpauses its token ID.
Prerequisites:
- Caller must have
UNSUSPEND - Bond must be in
Suspendedstatus - Token contract must not be globally paused
Parameters:
isin: Bond identifierversion: Exact version to unsuspend
Events:
BondUnsuspended(bytes12 indexed isin, uint8 indexed version, address indexed executor): Unsuspension status transition
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidBondStatus(bytes12): When bond is not inSuspendedstatusPausableUpgradeable.EnforcedPause(): When the token contract is globally paused
Sequence Diagram:
setAllowedCurrency(string calldata currencyCode, bool allowed)
Sets the allowed status for a given currency code.
Prerequisites:
- Caller must have
CURRENCY
Parameters:
currencyCode: Currency code (ISO 4217-style, exactly 3 uppercase ASCII letters)allowed: True to enable, false to disable
Events:
AllowedCurrencyUpdated(bytes3 currency, bool allowed, address changedBy): Emitted when currency is updated
Errors:
StringExtensions__InvalidBytesLength(): When the currency code is not exactly 3 bytesStringExtensions__NonAsciiCharacter(): When the currency code contains a byte outside 7-bit ASCIIStringExtensions__InvalidCharacter(): When the currency code contains control characters, lowercase letters, digits, punctuation, or other ASCII bytes outsideA-Z
Notes:
- This allowlist controls currencies in which new bonds may be denominated or changed into.
- Disabling a currency does not retroactively invalidate published bond versions that already store that currency.
- Marketplace settlement/pricing currencies are configured separately in
Marketplace; deployments must coordinate both lists according to the intended market configuration.
Sequence Diagram:
appendScoring(address wallet, Scoring calldata scoring)
Appends an issuer scoring record for a wallet.
Prerequisites:
- Caller must have
SCORING walletmust not be zeroscoring.issueDatemust be non-zeroscoring.expirationDatemust be greater thanscoring.issueDatescoring.distributorIdmust not be zeroscoring.defaultProbabilityBpsmust be at most10_000
Parameters:
wallet: Wallet whose issuer scoring history is being extendedscoring: Scoring record containing default probability in basis points, issue/expiration timestamps, and distributor identifier
Events:
ScoringAppended(address indexed wallet, uint256 indexed scoringId, uint16 defaultProbabilityBps, uint64 issueDate, uint64 expirationDate, bytes32 distributorId, address indexed caller): Emitted when a scoring record is appended
Errors:
ZeroAddress(): Whenwalletis zeroBondRegistry__ScoringZeroIssueDate(): WhenissueDate == 0BondRegistry__ScoringExpirationNotAfterIssue(): When expiration is not after issue dateBondRegistry__ScoringZeroDistributorId(): When distributor ID is zeroBondRegistry__ScoringProbabilityTooHigh(uint16): When default probability exceeds10_000
Sequence Diagram:
Administrative Functions
initialize(address owner_)
Initializes the bond registry contract.
Prerequisites:
- Contract must not be already initialized
Parameters:
owner_: Address that will own the contract
Sequence Diagram:
setMultiToken(address multiToken)
Sets the shared ERC-6909 token contract where all bond tokens are minted.
Prerequisites:
- Caller must be owner
- Token contract address must not be zero
- Token contract must implement
IBaseTokenand point back to thisBondRegistry - Shared token contract must not have been set before
Parameters:
multiToken: Address of the ERC-6909 token contract
Errors:
ZeroAddress(): When token contract address is zeroBondRegistry__InvalidMultiToken(address): When the token does not implementIBaseTokenor does not point back to thisBondRegistryBondRegistry__MultiTokenLocked(): When the shared token contract was already set
Events:
MultiTokenSet(address previousMultiToken, address multiToken): Dependency update event
Notes:
setMultiToken(...)is a one-time deployment wiring function. After the first successful call, the token dependency is locked and future calls revert withBondRegistry__MultiTokenLocked().
Sequence Diagram:
grantRoles(address user, uint256 roles)
Grants roles to a user. Only BondRegistry role bits are valid.
Prerequisites:
- Caller must be owner
- Only
PUBLISHER | CANCEL | CLOSE | CURRENCY | SUSPEND | UNSUSPEND | BURNER | SCORING | ISSUER_RECOVERYare valid
Errors:
InvalidRoles(): When attempting to grant invalid roles
Sequence Diagram:
View Functions
bondStatus(string calldata isin) / bondStatus(bytes12 isin)
Get bond status by ISIN. ISIN-only getters resolve activeVersion.
Returns:
BondStatus: The current status of the bond (returnsUnregisteredfor non-existent bonds)
getBond(string calldata isin) / getBond(bytes12 isin)
Get the currently active bond data for a given ISIN.
Returns:
Bond: The bond struct, including immutableisGuaranteedmetadata
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not exist
getBondAtVersion(bytes12 isin, uint8 version)
Get bond data at a specific version.
Returns:
Bond: The bond struct at the specified version, including immutableisGuaranteedmetadata
Errors:
BondRegistry__NonExistentBond(bytes12): When version does not exist
getTranche(bytes12 isin, uint16 trancheId)
Get a specific tranche record by tranche ID on the active version for the given ISIN.
Returns:
Tranche: The tranche struct (issueDate,issueCount)
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidTrancheId(uint16): When tranche ID is out of range
getTranche(bytes12 isin) / getTranche(string calldata isin)
Convenience getter — returns tranche 1 (the first issuance). Useful for the common single-tranche workflow.
Returns:
Tranche: The first tranche struct
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidTrancheId(uint16): When no tranches exist yet
getLatestTranche(bytes12 isin)
Get the most recently created tranche record.
Returns:
Tranche: The tranche withtrancheId == trancheCount
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not existBondRegistry__InvalidTrancheId(uint16): When no tranches exist yet
getTrancheCount(bytes12 isin)
Get the number of tranches issued for a bond.
Returns:
uint16: The number of tranches (0 if not yet issued)
Errors:
BondRegistry__NonExistentBond(bytes12): When bond does not exist
Bond metadata fields
Read coupon frequency, coupon rate type, currency, bond nominal value, issuance country, and maturity date through
getBond(bytes12 isin), getBond(string calldata isin), getBondAtVersion(bytes12 isin, uint8 version), or
getBondByTokenId(uint256 tokenId). BondRegistry intentionally does not expose separate single-field getters for
these fields.
getToken()
Get the shared ERC-6909 token contract address.
Returns:
address: The token contract address
getTranche(bytes12 isin, uint8 version, uint16 trancheId)
Get a specific tranche record for an exact bond version.
getTokenId(string calldata isin)
Get the ERC-6909 token ID for the active version of a bond family identified by its ISIN.
Returns:
uint256: The token ID used in the shared ERC-6909 token contract
getBondByTokenId(uint256 tokenId)
Reverse lookup: resolve the Bond record from a token ID. The reverse-index is written when a bond is published, so historical versions remain resolvable through their own token IDs.
Returns:
Bond: The bond struct associated with the given token ID, including immutableisGuaranteedmetadata
Errors:
BondRegistry__NonExistentBond(bytes12): When no ISIN is registered for the given token ID
getBondSeriesByTokenId(uint256 tokenId)
Reverse lookup: resolve the (isin, version) pair for a token ID.
Returns:
bytes12 isin: ISIN family linked to the token IDuint8 version: Exact bond version linked to the token ID
Errors:
BondRegistry__NonExistentBond(bytes12): When no bond version is registered for the given token ID
getActiveVersion(bytes12 isin)
Get the current active version for an ISIN family.
Returns:
uint8: Active version number, or0when the ISIN has not been registered
getLatestVersion(bytes12 isin)
Get the latest published version number for an ISIN family.
Returns:
uint8: Latest version number, or0when the ISIN has not been registered
getCurrentCouponRate(bytes12 isin)
Get the coupon rate applicable to a bond at the current block.timestamp.
Returns:
uint256: Current applicable coupon rate in basis points (0 forZERO_COUPONbonds, unissued bonds, timestamps before the first checkpoint, or timestamps at/after maturity)
getCurrentCouponRateForBond(Bond calldata bond)
Same as getCurrentCouponRate(isin) but operates on a caller-supplied Bond struct, avoiding a redundant storage load.
Returns:
uint256: Current applicable coupon rate in basis points (0 forZERO_COUPONbonds, unissued bonds, timestamps before the first checkpoint, or timestamps at/after maturity)
getAllCouponRates(string calldata isin)
Get all coupon rate checkpoints for the specified ISIN.
Returns:
uint256[] memory paymentTimestamps: Array of timestamp checkpointsuint256[] memory rates: Array of corresponding rates in basis points
getCouponRatesLength(string calldata isin)
Get the number of coupon rate checkpoints.
Returns:
uint256: The length of the coupon rates arrays
getCouponRateAt(string calldata isin, uint256 paymentTimestamp)
Get the coupon rate applicable at a specific timestamp using binary search.
Returns:
uint256: The applicable rate in basis points (0 if before first checkpoint or at/after maturity)
getLatestCouponRate(string calldata isin)
Get the latest non-zero coupon rate (the rate at the second-to-last checkpoint).
Returns:
uint256: The latest coupon rate in basis points
getScoringCount(address wallet)
Get the number of scoring records stored for a wallet.
Returns:
uint256: Number of scoring records
getScoringAt(address wallet, uint256 scoringId)
Get a specific scoring record by wallet-local scoring ID.
Returns:
Scoring: Stored scoring record
Errors:
BondRegistry__InvalidScoringId(address wallet, uint256 scoringId): WhenscoringIdis zero or greater than the wallet's scoring count
getLatestScoring(address wallet)
Get the latest scoring record for a wallet.
Returns:
Scoring: Most recently appended scoring record
Errors:
BondRegistry__ScoringNotFound(address wallet): When the wallet has no scoring records
isCurrencyAllowed(string memory currencyCode)
Checks whether the specified currency is allowed.
Parameters:
currencyCode: Currency code to check (ISO 4217-style, exactly 3 uppercase ASCII letters)
Returns:
bool: True if currency is allowed
Errors:
StringExtensions__InvalidBytesLength(): When the currency code is not exactly 3 bytesStringExtensions__NonAsciiCharacter(): When the currency code contains a byte outside 7-bit ASCIIStringExtensions__InvalidCharacter(): When the currency code contains control characters, lowercase letters, digits, punctuation, or other ASCII bytes outsideA-Z
supportsInterface(bytes4 interfaceId)
ERC-165 interface detection.
Returns:
bool: True ifIBondRegistryorIERC165interface is supported