Bonds on Bitcoin

sCrypt
7 min readDec 7, 2023

We are excited to introduce a method to issue and manage bonds directly on chain. Our method explores the integration of blockchain technology into the bond market, aiming to leverage its advantages to enhance efficiency and accessibility in the issuance, trading, and management of bonds. More specifically, we use smart contracts to automate and streamline various aspects of bond agreements, such as interest payments and bond redemptions, reducing the need for intermediaries and mitigating counterparty risk by providing a tamper-proof record of ownership and transactions.

Credit: Bitcoin Magazine

What is a Bond?

A bond is a debt security that represents a loan made by an investor to a borrower, typically a government or corporation. When an individual or entity purchases a bond, they are essentially lending money to the issuer in exchange for periodic interest payments and the return of the principal amount at the bond’s maturity date. A bond is referred to as a fixed-income instrument since bonds traditionally paid a fixed interest rate (coupon) to debt holders. They are key tools for raising capital and are fundamental to the financial markets.

Imagine that a corporation, ABC Inc., decides to raise capital for a new expansion project. To do this, ABC Inc. issues bonds with a face value of $1,000 each, a fixed interest rate (coupon rate) of 5%, and a maturity period of 10 years.

If an investor, let’s call them Investor A, purchases one of these bonds for $1,000, they are essentially lending $1,000 to ABC Inc. In return, ABC Inc. agrees to pay annual interest to Investor A at a rate of 5%, which amounts to $50 per year (5% of $1,000).

Over the 10-year period, Investor A will receive $50 in interest annually. At the end of the 10 years, ABC Inc. will return the initial principal amount of $1,000 to Investor A.

In summary:

  • Face value of the bond: $1,000
  • Annual interest rate: 5%
  • Annual interest payment: $50
  • Maturity period: 10 years

Zero-Coupon Bonds

Unlike regular bonds, zero-coupon bonds are issued at a discount and do not pay periodic interest. The investor’s return is realized when the bond matures at its face value.

Let’s say Company XYZ issues a zero-coupon bond with a face value of $1,000 and a maturity period of 5 years. However, this bond is issued at a discount, meaning the investor doesn’t pay the full face value upfront. Instead, they might purchase the bond for $800.

In this case:

  • Face value of the zero-coupon bond: $1,000
  • Purchase price (discounted): $800
  • Maturity period: 5 years

Implementation

Zero-Coupon Bond

Our initial implementation features a zero-coupon bond smart contract, where a single investor lends capital to a borrower. This simpler bond structure is ideal for demonstrating the basics of bond smart contracts, acting as a basis for more complicated bonds. The smart contract is designed to automate the process of issuing, trading, and redeeming a zero-coupon bond. The contract includes several key public methods:

  • buy: handles the purchase of the bond, transferring ownership to a new investor.
  • mature: executed by the issuer to pay the face value to the investor upon bond maturity.
  • listForSale: allows the current investor to list the bond for sale.
  • cancelSale: enables the investor to cancel the sale of the bond.
  • default: in case of a default, this method allows the investor to claim the assets locked in the contract.

The bond has the following lifecycle.

  1. Issuance and Sale: an issuer initializes the bond with its face value, maturity time, and initial price. The bond can then be bought by an investor.
  2. Trading: investors can trade the bond on the secondary market by listing it for sale and transferring ownership through the buy method.
  3. Maturity and Redemption: upon reaching maturity, the bond’s face value is paid to the current investor by the issuer.
  4. Default Handling: in case the bond defaults (issuer fails to pay at maturity), the default method provides a mechanism for the investor to claim compensation.
class ZeroCouponBond extends SmartContract {
@prop()
issuer: PubKey

@prop(true)
investor: PubKey

@prop(true)
forSale: boolean

// Price of the bond in satoshis.
@prop(true)
price: bigint

@prop()
faceValue: bigint

@prop()
matureTime: bigint

...

@method()
public buy(newInvestor: PubKey) {
const prevInvestor = this.investor
// Set new investor.
this.investor = newInvestor
// Toggle for sale flag.
this.forSale = false
let outputs = toByteString('')
const alreadyOwned =
this.investor ==
PubKey(
toByteString(
'0000000000000000000000000000000000000000000000000000000000000000'
)
)
if (alreadyOwned) {
// Pay previous investor.
outputs += this.buildStateOutput(this.ctx.utxo.value)
outputs += Utils.buildAddressOutput(
pubKey2Addr(prevInvestor),
this.price
)
} else {
// Deposit to contract.
outputs += this.buildStateOutput(this.ctx.utxo.value + this.price)
}
// Enforce outputs.
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

@method()
public mature(issuerSig: Sig) {
// Check issuer signature.
assert(this.checkSig(issuerSig, this.issuer), 'invalid sig issuer')
// Check mature time passed.
assert(this.timeLock(this.matureTime), 'bond not matured')
// Ensure investor gets payed face value of the bond.
let outputs = Utils.buildAddressOutput(
pubKey2Addr(this.investor),
this.faceValue
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

@method()
public listForSale(price: bigint, investorSig: Sig) {
// Check investor signature.
assert(
this.checkSig(investorSig, this.investor),
'invalid sig investor'
)
// Set price and toggle for sale flag.
this.price = price
this.forSale = true
// Propagate contract.
let outputs = this.buildStateOutput(this.ctx.utxo.value)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

...

@method()
public default(investorSig: Sig) {
// After default deadline is reached the investor can
// take everything locked within the smart contract...
// Check investor signature.
assert(
this.checkSig(investorSig, this.investor),
'invalid sig investor'
)
// Check mature time + ~14 days.
assert(
this.timeLock(this.matureTime + 20160n),
'deadline for default not reached'
)
}
}

In our zero-coupon bond smart contract, the issuer must lock a fraction of the bond’s face value in satoshis as collateral during deployment. This feature enhances investor security, acting as a safeguard in case of default. If the issuer fails to pay at maturity, the investor can claim this collateral.

However, for issuers with established credibility, this collateral requirement can be optional. This flexibility allows trustworthy issuers to opt out of locking collateral, making the bond issuance more streamlined and cost-effective.

Transform into a Regular Coupon Bond

The transition from a zero-coupon bond to a regular coupon bond in our smart contract is achieved by adding a makePayment method. This method enables the issuer to make periodic interest payments to the investor.

@method()
public makePayment(issuerSig: Sig) {
// Check issuer signature.
assert(this.checkSig(issuerSig, this.issuer), 'invalid sig issuer')

let outputs = this.buildStateOutput(this.ctx.utxo.value)
// Ensure investor gets payed interest.
const interest = (this.faceValue * this.interestRate) / 100n
outputs += Utils.buildAddressOutput(
pubKey2Addr(this.investor),
this.faceValue
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

Support Multiple Investors

While it is still possible to support multiple investors with the former smart contract by just deploying multiple instances, the sum of which represent the totality of the bond, it is also possible to keep track of multiple investors within the same instance. Instead of only storing a single investor’s public key, we can store an array of investors.

type Investor = {
emptySlot: boolean
pubKey: PubKey
forSale: boolean
price: bigint
}

class CouponBond extends SmartContract {
static readonly N_INVESTORS = 10
@prop(true)
investors: FixedArray<Investor, typeof Bsv20ZeroCouponBond.N_INVESTORS>
...
}

In this case the methods of our bond contract need to be adjusted in order to manage this array. For example in the method used to make an investment, we would implement something like the following:

@method()
public invest(
slotIdx: bigint,
investorPubKey: PubKey
) {
...

// Check slot index is empty.
const investor = this.investors[Number(slotIdx)]
assert(investor.emptySlot == true, 'slot is not empty')
// Add to investors array.
this.investors[Number(slotIdx)] = {
emptySlot: false,
pubKey: investorPubKey,
forSale: false,
price: 0n,
}
// Ensure that investor pays issuer asked purchase price
// and propagate contract.
...
}

Fiat Denomination

In practice, a bond is likely denominated in some type of fiat value like USD or CNY, instead of satoshis as shown so far. We can integrate aforementioned bond contracts with BSV-20 tokens, representing fiat units.

The high-level workings of the contract stays mostly the same, however payments represent special token transfers, which the smart contract needs to properly handle. For this we can employ the scrypt-ord SDK.

For example, the invest public method might look something like this:

@method()
public invest(
slotIdx: bigint,
investorPubKey: PubKey,
investorSig: Sig,
oracleMsg: ByteString,
oracleSig: RabinSig
) {
// Check investor sig.
assert(
this.checkSig(investorSig, investorPubKey),
'invalid sig investor'
)

// Check oracle signature.
assert(
RabinVerifier.verifySig(oracleMsg, oracleSig, this.oraclePubKey),
'oracle sig verify failed'
)
// Check that we're unlocking the UTXO specified in the oracles message.
assert(
slice(this.prevouts, 0n, 36n) == slice(oracleMsg, 0n, 36n),
'first input is not spending specified ordinal UTXO'
)
// Get token amount held by the UTXO from oracle message.
const utxoTokenAmt = byteString2Int(slice(oracleMsg, 36n, 44n))
// Ensure token amount is equal to the purchase price.
assert(
utxoTokenAmt == this.purchasePrice,
'utxo token amount insufficient'
)
// Check slot index is empty.
const investor = this.investors[Number(slotIdx)]
assert(investor.emptySlot == true, 'slot is not empty')
// Add to investors array.
this.investors[Number(slotIdx)] = {
emptySlot: false,
pubKey: investorPubKey,
forSale: false,
price: 0n,
}
// Ensure that investor pays issuer asked purchase price
// and propagate contract.
let outputs = this.buildStateOutput(this.ctx.utxo.value)
outputs += BSV20V2.buildTransferOutput(
pubKey2Addr(this.issuer),
this.id,
this.purchasePrice
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

Conclusion

Full code examples are accessible on GitHub:

--

--

sCrypt

sCrypt (https://scrypt.io) is a full-stack smart contract and token development platform for Bitcoin