EDIT: Full code available here.
The following idea is motivated by the paper “The Yield Protocol: On-Chain Lending With Interest Rate Discovery” which uses the Ethereum model.
I think the same thing can be implemented in Ergo in a possibly easier way.
The following uses the idea of “zero-coupon bonds”, which are essentially tokens issued by a company at a discounted value (at 0.9 USD per token, for example) and redeemable at 1:1 rate after some future date.
While in the real world, such bonds are backed by the trust of the company that issues them, in Ergo, we can remove the trust and make the bonds backed by collateral in Ergs. The benefit for the issuer is the potential to profit if the price of Ergs goes up, and the benefit of the buyer is the potential to benefit if the price of ergs goes down, along with the trustless nature because of collateral that can be instantly liquidated in a margin call.
While the tokens are themselves exchangeable using only the contract that issued them, we can have another contract that functions as a decentralized exchange (DEX) on top that allows them to be fully fungible. This is a topic for another post.
The following describes a bond for a singler user, Alice.
The bond is represented using two boxes. The first is a “bond box”, which stores the tokens used as the bond, along with the collateral in Ergs. The second is a “liquidated box”, that stores the collateral in Ergs after the bond box is liquidated. A box is liquidated either when the bond expires or in the event of a margin call. Alice can prevent a margin call by topping up the box with Ergs. This is one place where “trust” comes into the picture; if people trust Alice to top up the box, then her tokens can potentially be valued higher. However, since we can instantly liquidate the box and then purchase new tokens (at a possibly discounted rate) this is not a major issue.
Since the bond box references the liquidated box, we describe the latter first.
{ // liquidatedBoxSource
val bondOwner = proveDlog(alice)
val fixedRate = SELF.R4[Long].get // nanoErgs per usdCent at time of liquidation
val maxRedeemTime = SELF.R5[Int].get
val tokenID = SELF.tokens(0)._1 // tokenID that maps to bonds
val tokenNum = SELF.tokens(0)._2 // how many bond tokens left
val newBox = OUTPUTS(0)
val newBoxTokenID = newBox.tokens(0)._1
val newBoxTokenNum = newBox.tokens(0)._2 // how many bond tokens left
val bondDiff = newBoxTokenNum - tokenNum
val ergsDiff = SELF.value - newBox.value
val validNewBox = newBox.propositionBytes == SELF.propositionBytes &&
newBoxTokenID == tokenID &&
bondDiff >= 10000 && // at least 100 USD difference (prevent micro tx)
ergsDiff <= bondDiff * fixedRate &&
newBox.R4[Long].get == fixedRate &&
newBox.R5[Int].get == maxRedeemTime
(bondOwner &&
(HEIGHT > maxRedeemTime)
) || validNewBox
}
The bond box is defined using the script:
{ // bondBoxSource
val numBonds = SELF.R4[Long].get // how many bonds issued (one bond = 1 USD cent)
val tokenID = SELF.tokens(0)._1 // tokenID that maps to bonds
val tokenNum = SELF.tokens(0)._2 // how many bond tokens left
val newBox = OUTPUTS(0)
val newBoxTokenID = newBox.tokens(0)._1
val newBoxTokenNum = newBox.tokens(0)._2 // how many bond tokens left
val validNewBoxToken = tokenID == newBoxTokenID
val rateBox = CONTEXT.dataInputs(0)
val rate = rateBox.R4[Long].get // nanoErgs per usdCent
val validRateBox = rateBox.tokens(0)._1 == rateTokenID
val lockedErgs = SELF.value // nanoErgs
val neededErgs = numBonds * rate
val insufficientErgs = lockedErgs * 10 >= neededErgs * 11 // at least 10 percent margin
if (HEIGHT > endHeight || insufficientErgs) {
// bond ended or margin call
blake2b256(newBox.propositionBytes) == liquidatedBoxScriptHash &&
validNewBoxToken && newBoxTokenNum == tokenNum &&
newBox.R4[Long].get == rate &&
newBox.R5[Int].get >= HEIGHT + withdrawDeadline
} else {
// purchase bonds
val numTokensReduced = tokenNum - newBoxTokenNum
val numNewBonds = newBox.R4[Long].get
val numBondsIncreased = numNewBonds - numBonds
val ergsIncreased = newBox.value - SELF.value
val validErgsIncrease = ergsIncreased >= numBondsIncreased * rate
newBox.propositionBytes == SELF.propositionBytes &&
numBondsIncreased >= minBondsToPurchase &&
numBondsIncreased == numTokensReduced &&
validErgsIncrease &&
numNewBonds <= maxBonds
}
}
The environment variables used above are:
-
rateOracleTokenID
is id of oracle that publishes rate -
alice
is a group element representing issuer pub key -
liquidatedBoxScriptHash
is hash of liquidated box script -
maxBonds
is maximum number of bonds to be issued. Note that one bond is equivalent to 1 USD cent. -
minBondsToPurchase
is minimum number of bonds in one purchase -
endHeight
is height at which the bond expires (and can be redeemed) -
withdrawDeadline
is the minimum amount issuer must wait after liquidation before he can keep the balance.