Based on the above description and some small changes, one variant of the loan contract is given at this link
{
val dataInput = CONTEXT.dataInputs(0)
val rate = dataInput.R4[Long].get // rate (how many nano-Erg for 1 USD cent)
// Check 1: CORRECT RATE ORACLE (has the correct token id)
val correctRateOracle = dataInput.tokens(0)._1 == rateOracleTokenID
val out = OUTPUTS(0) // should be same box script
val currentUSD = SELF.R4[Long].get // how many USD owed to Bob
val lastPaymentHeight = SELF.R5[Int].get // what height last payment was made
val thisPaymentHeight = out.R5[Int].get // what the current height is
// Check 2: CORRECT HEIGHT (within a day of current HEIGHT and higher than previous height)
val correctHeight = thisPaymentHeight <= HEIGHT &&
thisPaymentHeight >= HEIGHT - 720 && // within a day
thisPaymentHeight > lastPaymentHeight
// Check 3: CORRECT SCRIPT
val correctScript = out.propositionBytes == SELF.propositionBytes
val outUSD = out.R4[Long].get
val usdDiff = currentUSD - outUSD
val ergsDiff = SELF.value - out.value
val correctErgsDiff = usdDiff * rate == ergsDiff
// Check 4: CORRECT Ergs difference
val correctDiff = usdDiff == emi && correctErgsDiff
val correctTx = correctDiff && correctScript && correctRateOracle && correctHeight
// Four different ways box can be spent
//
// 1. Alice makes 10 Euro payment within 35 days of last payment
val bobBox = OUTPUTS(1) // this is the box where Alice will pay to Bob
val correctBobAmt = bobBox.tokens(0)._1 == usdTokenID && bobBox.tokens(0)._2 == emi
val correctBobScript = bobBox.propositionBytes == proveDlog(bob).propBytes
val correctBobBox = correctBobAmt && correctBobScript
val payment = correctTx && proveDlog(alice) && correctBobBox
// 2. Alice does not make payment within 35 days of last payment. Bob takes out due himself
val nonPayment = correctTx && proveDlog(bob) && ((HEIGHT - lastPaymentHeight) > (oneMonth + fiveDays))
// 3. Price drops anytime (margin call)
val marginCall = (currentUSD * rate > SELF.value) && proveDlog(bob)
// 4. Price increases anytime (profit sharing)
val reqd = currentUSD * rate * 12 / 10
val profit = (SELF.value - reqd)/2
val ergPriceHigh = profit > 0
val profitSharing = ergPriceHigh && correctScript && out.value == reqd && usdDiff == 0 &&
OUTPUTS(1).propositionBytes == proveDlog(bob).propBytes && OUTPUTS(1).value == profit &&
OUTPUTS(2).propositionBytes == proveDlog(alice).propBytes && OUTPUTS(2).value == profit &&
lastPaymentHeight == thisPaymentHeight
anyOf(
Coll(
profitSharing,
marginCall,
payment,
nonPayment
)
)
}
This is based on the One-Way USD Token idea described in this post, and whose code is given here.
To quote from that post:
The one-way convertible token is defined as follows:
Bob can create a “token box” with a large number of “USD tokens” that can be exchanged for Ergs at the current rate in USD/Erg. The contract in the token box only allows changing Ergs to tokens and not the other way round.
So Bob could create a box with 1 trillion tokens such that anyone can pay ergs and purchase tokens at the inverse rate of USD/ergs. If the rate is 10 USD/Erg, then anyone can purchase X number of tokens by paying X/10 Ergs.
One-way USD token (1USD) allows Alice to pay the loan back without Bob’s intervention and takes care of the cheating Bob scenario discussed above, where Bob refuses to acknowledge Alice’s repayment.
The other way Bob could attack the system is to make the 1USD tokens unavailable for purchase. This can be handled by keeping a large number of tokens that can never be exhausted using the available ERGs. The code of 1USD token is reproduced below
{
val newSelf = OUTPUTS(0) // new box created as a replica of current box
val bobOut = OUTPUTS(1) // box paying to Bob
val bobNanoErgs = bobOut.value
val validBobBox = bobOut.propositionBytes == proveDlog(bob).propBytes
val selfTokenID = SELF.tokens(0)._1
val selfTokenAmt = SELF.tokens(0)._2
val newSelfTokenID = newSelf.tokens(0)._1
val newSelfTokenAmt = newSelf.tokens(0)._2
val validTokenID = selfTokenID == newSelfTokenID
val validProp = newSelf.propositionBytes == SELF.propositionBytes
val tokenDiff = selfTokenAmt - newSelfTokenAmt
val validNewSelf = validTokenID && validProp
val rateBox = CONTEXT.dataInputs(0)
val rate = rateBox.R4[Long].get
val validRateBox = rateBox.tokens(0)._1 == rateTokenID
// rate gives nanoErg per USDCent
// Thus, bobNanoErgs NanoErgs will cost bobNanoErgs / rate usd cents
val usdCDiff = bobNanoErgs / rate
tokenDiff <= usdCDiff && validRateBox && validNewSelf && validBobBox
}