Skip to content

Commit

Permalink
Add regular bsv-20 coupon bond.
Browse files Browse the repository at this point in the history
  • Loading branch information
msinkec committed Dec 1, 2023
1 parent e8d97a1 commit 0afae6c
Showing 1 changed file with 274 additions and 0 deletions.
274 changes: 274 additions & 0 deletions src/contracts/bsv20CouponBond.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { BSV20V2 } from 'scrypt-ord'
import {
assert,
ByteString,
byteString2Int,
fill,
FixedArray,
hash256,
method,
prop,
PubKey,
pubKey2Addr,
Sig,
slice,
toByteString,
Utils,
} from 'scrypt-ts'
import { RabinPubKey, RabinSig, RabinVerifier } from 'scrypt-ts-lib'

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

export class Bsv20CouponBond extends BSV20V2 {
static readonly N_INVESTORS = 10

@prop()
issuer: PubKey

@prop(true)
investors: FixedArray<Investor, typeof Bsv20CouponBond.N_INVESTORS>

@prop()
faceValue: bigint

@prop()
interestRate: bigint // 1n == 1%

@prop()
matureTime: bigint

@prop()
oraclePubKey: RabinPubKey

constructor(
id: ByteString,
sym: ByteString,
max: bigint,
dec: bigint,
issuer: PubKey,
faceValue: bigint,
interestRate: bigint,
matureTime: bigint,
oraclePubKey: RabinPubKey
) {
super(id, sym, max, dec)
this.init(...arguments)

this.issuer = issuer
this.investors = fill(
{
emptySlot: true,
pubKey: PubKey(
toByteString(
'000000000000000000000000000000000000000000000000'
)
),
forSale: false,
price: 0n,
} as Investor,
10
)
this.faceValue = faceValue
this.interestRate = interestRate
this.matureTime = matureTime
this.oraclePubKey = oraclePubKey
}

@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 face value.
assert(utxoTokenAmt == this.faceValue, '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 face value
// and propagate contract.
let outputs = this.buildStateOutput(this.ctx.utxo.value)
outputs += BSV20V2.buildTransferOutput(
pubKey2Addr(this.issuer),
this.id,
this.faceValue
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

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

// 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'
)

// For each investor add an output that pays them interest.
const interest = (this.faceValue * this.interestRate) / 100n
let outputs = toByteString('')
let totalAmt = 0n
for (let i = 0; i < Bsv20CouponBond.N_INVESTORS; i++) {
const investor = this.investors[i]
if (!investor.emptySlot) {
outputs += BSV20V2.buildTransferOutput(
pubKey2Addr(investor.pubKey),
this.id,
interest
)
totalAmt += interest
}
}

// Get token amount held by the UTXO from oracle message.
const utxoTokenAmt = byteString2Int(slice(oracleMsg, 36n, 44n))

// Ensure utxo token amount covers output token amount.
assert(utxoTokenAmt >= totalAmt, 'utxo token amount insufficient')

// Enforce outputs.
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

@method()
public mature(issuerSig: Sig, oracleMsg: ByteString, oracleSig: RabinSig) {
// Check issuer sig.
assert(this.checkSig(issuerSig, this.issuer), 'invalid sig issuer')

// Check mature time.
assert(this.timeLock(this.matureTime), 'not matured yet')

// 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'
)

// For each investor add an output that pays them the face value token amount.
let outputs = toByteString('')
let totalAmt = 0n
for (let i = 0; i < Bsv20CouponBond.N_INVESTORS; i++) {
const investor = this.investors[i]
if (!investor.emptySlot) {
outputs += BSV20V2.buildTransferOutput(
pubKey2Addr(investor.pubKey),
this.id,
this.faceValue
)
totalAmt += this.faceValue
}
}

// Get token amount held by the UTXO from oracle message.
const utxoTokenAmt = byteString2Int(slice(oracleMsg, 36n, 44n))

// Ensure utxo token amount covers output token amount.
assert(utxoTokenAmt >= totalAmt, 'utxo token amount insufficient')

// Enforce outputs.
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

@method()
public listForSale(slotIdx: bigint, price: bigint, investorSig: Sig) {
const investor = this.investors[Number(slotIdx)]

// Check investor sig.
assert(
this.checkSig(investorSig, investor.pubKey),
'invalid sig investor'
)

// Toggle forSale flag and set price.
this.investors[Number(slotIdx)].forSale = true
this.investors[Number(slotIdx)].price = price

// Propagate contract.
let outputs = this.buildStateOutput(this.ctx.utxo.value)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}

@method()
public buy(slotIdx: bigint, newOwner: PubKey, newOwnerSig: Sig) {
const investor = this.investors[Number(slotIdx)]

// Check new owner sig.
assert(this.checkSig(newOwnerSig, newOwner), 'invalid sig investor')

// Check bond is for sale.
assert(investor.forSale, 'bond not for sale')

// Toggle forSale flag.
this.investors[Number(slotIdx)].forSale = false

// Propagate contract and ensure new owner pays seller of the bond.
let outputs = this.buildStateOutput(this.ctx.utxo.value)
outputs += Utils.buildAddressOutput(
pubKey2Addr(investor.pubKey),
investor.price
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
}

0 comments on commit 0afae6c

Please sign in to comment.