pragma arkade ^0.1.0;
// The pool only. Deposits and exits are separate contracts.
// One sweep spends this output once and up to four standing outputs. Output i settles input i.
// feeBps is the fixed fee paid on repay. satsPerUnit is the collateral sats required per stable unit.
// Lender supply and debt supply are ledger assets on this output, so this script stays fixed.
// noExitKey is the BIP341 NUMS point: 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
contract LendingPool(
bytes32 stableTxid, int stableGidx,
bytes32 lenderTxid, int lenderGidx,
bytes32 collTxid, int collGidx,
bytes32 debtTxid, int debtGidx,
bytes32 lenderLedgTxid, int lenderLedgGidx,
bytes32 debtLedgTxid, int debtLedgGidx,
bytes32 ctrlTxid, int ctrlGidx,
int feeBps, int satsPerUnit,
pubkey noExitKey
) {
const int DUST = 330;
private function samePool() {
require(tx.outputs[0].scriptPubKey == tx.input.current.scriptPubKey, "pool");
}
private function batch() {
require(this.activeInputIndex == 0, "pool input");
require(tx.numInputs >= 2, "batch");
require(tx.numInputs <= 5, "batch");
require(tx.numOutputs == tx.numInputs, "outputs");
samePool();
}
private function held(bytes32 txid, int gidx) int {
let row = tx.inputs[0].assets.tryLookup(txid, gidx);
if (row.found) {
return row.amount;
}
return 0;
}
private function keepCtrl() {
require(tx.outputs[0].assets.lookup(ctrlTxid, ctrlGidx) == 1, "ctrl");
let ctrlGroup = tx.assetGroups.find(ctrlTxid, ctrlGidx);
require(ctrlGroup.delta == 0, "ctrl supply");
}
// Sweep standing stablecoin deposits. Mint one lender share per unit.
function sweepLender() {
batch();
int added = 0;
int[4] slots = [1, 2, 3, 4];
for (i, slot) in slots {
if (tx.numInputs > slot) {
int part = tx.inputs[slot].assets.lookup(stableTxid, stableGidx);
require(tx.outputs[slot].assets.lookup(lenderTxid, lenderGidx) == part, "shares");
added = added + part;
}
}
require(added > 0, "stable");
int prev = held(stableTxid, stableGidx);
int ledg = held(lenderLedgTxid, lenderLedgGidx);
require(tx.outputs[0].value == tx.inputs[0].value, "btc");
keepCtrl();
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev + added, "stable");
require(tx.outputs[0].assets.lookup(lenderLedgTxid, lenderLedgGidx) == ledg + added, "ledger");
let shareGroup = tx.assetGroups.find(lenderTxid, lenderGidx);
require(shareGroup.delta == added, "issued");
require(shareGroup.controlIs(ctrlTxid, ctrlGidx), "control");
let ledgGroup = tx.assetGroups.find(lenderLedgTxid, lenderLedgGidx);
require(ledgGroup.delta == added, "ledger");
require(ledgGroup.controlIs(ctrlTxid, ctrlGidx), "ledger control");
}
// Sweep standing bitcoin deposits. Mint one collateral token per sat above the carrier.
function sweepBorrower() {
batch();
int added = 0;
int[4] slots = [1, 2, 3, 4];
for (i, slot) in slots {
if (tx.numInputs > slot) {
int part = tx.inputs[slot].value - DUST;
require(tx.outputs[slot].assets.lookup(collTxid, collGidx) == part, "collateral token");
added = added + part;
}
}
require(added > 0, "collateral");
int prev = held(stableTxid, stableGidx);
keepCtrl();
require(tx.outputs[0].value == tx.inputs[0].value + added, "btc");
if (prev == 0) {
require(!tx.outputs[0].assets.has(stableTxid, stableGidx), "no stable");
} else {
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev, "stable");
}
let collGroup = tx.assetGroups.find(collTxid, collGidx);
require(collGroup.delta == added, "issued");
require(collGroup.controlIs(ctrlTxid, ctrlGidx), "control");
}
// Take stablecoin against collateral tokens the holder keeps.
function borrow(int take) {
require(this.activeInputIndex == 0, "pool input");
require(tx.numInputs == 3, "pool, collateral, carriers");
require(take > 0, "take");
require(satsPerUnit > 0, "ratio");
require(tx.inputs[1].value == DUST, "coll carrier");
require(tx.inputs[1].assets.length == 1, "coll only");
int coll = tx.inputs[1].assets.lookup(collTxid, collGidx);
require(coll >= take * satsPerUnit, "collateral");
require(tx.inputs[2].assets.length == 0, "sats");
require(tx.inputs[2].value == DUST + DUST, "carriers");
int prev = held(stableTxid, stableGidx);
require(prev >= take, "liquidity");
int debt = held(debtLedgTxid, debtLedgGidx);
int collSats = tx.inputs[0].value - DUST;
require(collSats >= (debt + take) * satsPerUnit, "pool ratio");
require(tx.numOutputs == 3, "three outputs");
samePool();
require(tx.outputs[0].value == tx.inputs[0].value, "btc");
keepCtrl();
if (prev == take) {
require(!tx.outputs[0].assets.has(stableTxid, stableGidx), "drained");
} else {
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev - take, "stable");
}
require(tx.outputs[0].assets.lookup(debtLedgTxid, debtLedgGidx) == debt + take, "debt ledger");
let holder = tx.inputs[1].scriptPubKey;
require(tx.outputs[1].scriptPubKey == holder, "holder");
require(tx.outputs[1].value == DUST, "coll carrier");
require(tx.outputs[1].assets.lookup(collTxid, collGidx) == coll, "coll back");
require(tx.outputs[1].assets.length == 1, "coll only");
require(tx.outputs[2].scriptPubKey == holder, "holder");
require(tx.outputs[2].value == DUST, "debt carrier");
require(tx.outputs[2].assets.lookup(debtTxid, debtGidx) == take, "debt");
require(tx.outputs[2].assets.lookup(stableTxid, stableGidx) == take, "stable out");
require(tx.outputs[2].assets.length == 2, "debt and stable");
let collGroup = tx.assetGroups.find(collTxid, collGidx);
require(collGroup.delta == 0, "coll supply");
let debtGroup = tx.assetGroups.find(debtTxid, debtGidx);
require(debtGroup.delta == take, "debt issued");
require(debtGroup.controlIs(ctrlTxid, ctrlGidx), "control");
let ledgGroup = tx.assetGroups.find(debtLedgTxid, debtLedgGidx);
require(ledgGroup.delta == take, "ledger");
require(ledgGroup.controlIs(ctrlTxid, ctrlGidx), "ledger control");
let stableGroup = tx.assetGroups.find(stableTxid, stableGidx);
require(stableGroup.delta == 0, "stable supply");
}
// Burn debt. The stablecoin returned is the debt plus the fixed fee.
function repay() {
require(this.activeInputIndex == 0, "pool input");
require(tx.numInputs == 3, "pool, debt, payment");
require(tx.inputs[1].assets.length == 1, "debt only");
require(tx.inputs[1].value == DUST, "debt carrier");
int owed = tx.inputs[1].assets.lookup(debtTxid, debtGidx);
require(owed > 0, "debt");
int due = owed + (owed * feeBps) / 10000;
require(tx.inputs[2].value == DUST, "pay carrier");
require(tx.inputs[2].assets.length == 1, "stable only");
require(tx.inputs[2].assets.lookup(stableTxid, stableGidx) == due, "fee");
int prev = held(stableTxid, stableGidx);
int debt = tx.inputs[0].assets.lookup(debtLedgTxid, debtLedgGidx);
require(owed <= debt, "ledger");
require(tx.numOutputs == 2, "two outputs");
samePool();
require(tx.outputs[0].value == tx.inputs[0].value, "btc");
keepCtrl();
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev + due, "stable");
if (debt == owed) {
require(!tx.outputs[0].assets.has(debtLedgTxid, debtLedgGidx), "debt cleared");
} else {
require(tx.outputs[0].assets.lookup(debtLedgTxid, debtLedgGidx) == debt - owed, "debt ledger");
}
require(tx.outputs[1].scriptPubKey == tx.inputs[1].scriptPubKey, "holder");
require(tx.outputs[1].value == DUST + DUST, "carriers back");
require(tx.outputs[1].assets.length == 0, "no assets");
let debtGroup = tx.assetGroups.find(debtTxid, debtGidx);
require(debtGroup.sumInputs == owed, "debt in");
require(debtGroup.sumOutputs == 0, "burned");
require(debtGroup.controlIs(ctrlTxid, ctrlGidx), "control");
let ledgGroup = tx.assetGroups.find(debtLedgTxid, debtLedgGidx);
require(ledgGroup.delta == 0 - owed, "ledger");
require(ledgGroup.controlIs(ctrlTxid, ctrlGidx), "ledger control");
let stableGroup = tx.assetGroups.find(stableTxid, stableGidx);
require(stableGroup.delta == 0, "stable supply");
}
// Sweep lender staging outputs. Burn the shares and pay each its fraction of the stablecoin.
function withdrawLender() {
batch();
int supply = tx.inputs[0].assets.lookup(lenderLedgTxid, lenderLedgGidx);
int prev = tx.inputs[0].assets.lookup(stableTxid, stableGidx);
require(supply > 0, "supply");
int burned = 0;
int paid = 0;
int[4] slots = [1, 2, 3, 4];
for (i, slot) in slots {
if (tx.numInputs > slot) {
int part = tx.inputs[slot].assets.lookup(lenderTxid, lenderGidx);
int partPay = (part * prev) / supply;
require(partPay > 0, "payout");
require(tx.outputs[slot].assets.lookup(stableTxid, stableGidx) == partPay, "stable out");
burned = burned + part;
paid = paid + partPay;
}
}
require(burned <= supply, "supply");
require(prev >= paid, "liquidity");
require(tx.outputs[0].value == tx.inputs[0].value, "btc");
keepCtrl();
if (prev == paid) {
require(!tx.outputs[0].assets.has(stableTxid, stableGidx), "drained");
} else {
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev - paid, "stable");
}
if (supply == burned) {
require(!tx.outputs[0].assets.has(lenderLedgTxid, lenderLedgGidx), "ledger cleared");
} else {
require(tx.outputs[0].assets.lookup(lenderLedgTxid, lenderLedgGidx) == supply - burned, "ledger");
}
let shareGroup = tx.assetGroups.find(lenderTxid, lenderGidx);
require(shareGroup.sumInputs == burned, "shares in");
require(shareGroup.sumOutputs == 0, "burned");
require(shareGroup.controlIs(ctrlTxid, ctrlGidx), "control");
let ledgGroup = tx.assetGroups.find(lenderLedgTxid, lenderLedgGidx);
require(ledgGroup.delta == 0 - burned, "ledger");
require(ledgGroup.controlIs(ctrlTxid, ctrlGidx), "ledger control");
let stableGroup = tx.assetGroups.find(stableTxid, stableGidx);
require(stableGroup.delta == 0, "stable supply");
}
// Sweep borrower staging outputs. Burn the collateral tokens and pay that many sats.
function withdrawColl() {
batch();
int burned = 0;
int[4] slots = [1, 2, 3, 4];
for (i, slot) in slots {
if (tx.numInputs > slot) {
int part = tx.inputs[slot].assets.lookup(collTxid, collGidx);
require(tx.outputs[slot].value == part + DUST, "btc out");
burned = burned + part;
}
}
require(burned > 0, "burned");
require(tx.inputs[0].value >= burned + DUST, "btc");
int debt = held(debtLedgTxid, debtLedgGidx);
int left = tx.inputs[0].value - DUST - burned;
require(left >= debt * satsPerUnit, "ratio");
int prev = held(stableTxid, stableGidx);
require(tx.outputs[0].value == tx.inputs[0].value - burned, "btc");
keepCtrl();
if (prev == 0) {
require(!tx.outputs[0].assets.has(stableTxid, stableGidx), "no stable");
} else {
require(tx.outputs[0].assets.lookup(stableTxid, stableGidx) == prev, "stable");
}
let collGroup = tx.assetGroups.find(collTxid, collGidx);
require(collGroup.sumInputs == burned, "coll in");
require(collGroup.sumOutputs == 0, "burned");
require(collGroup.controlIs(ctrlTxid, ctrlGidx), "control");
}
function unilateral(signature noExitSig) tapscript {
require(older(serverExitDelay));
require(checkSig(noExitSig, noExitKey));
}
}