Skip to content

Commit 65dc004

Browse files
committed
Finish first iteration of the contract
1 parent 4dc72cb commit 65dc004

1 file changed

Lines changed: 101 additions & 18 deletions

File tree

on-chain/validators/synth-dolar.ak

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,47 @@
11
use aiken/collection/dict
2+
use utils.{compute_expected_synth_amount, health_ratio}
23
use aiken/collection/list
34
use cardano/address.{Script}
45
use cardano/assets.{PolicyId, Value, lovelace_of, tokens}
5-
use cardano/transaction.{Transaction, OutputReference}
6+
use cardano/transaction.{Transaction, OutputReference, InlineDatum}
67
use pyth
78
use pyth.{PriceUpdate}
89
use types/u32
910

11+
pub type PoolDatum {
12+
owner: ByteArray
13+
}
14+
1015
pub type Action {
1116
Mint
1217
Burn
1318
Liquidate
1419
}
1520

16-
validator synth_dolar(pyth_policy_id: PolicyId, ada_usd_feed_id: Int) {
17-
mint(_redeemer: Action, _policy_id: PolicyId, _self: Transaction) {
21+
validator synth_dolar(
22+
pyth_policy_id: PolicyId,
23+
ada_usd_feed_id: Int,
24+
collateral_ratio: Int,
25+
liquidation_threshold: Int,
26+
) {
27+
mint(redeemer: Action, policy_id: PolicyId, self: Transaction) {
1828
let (raw_price, exponent) =
19-
get_ada_usd_price(pyth_policy_id, ada_usd_feed_id, _self)
29+
get_ada_usd_price(pyth_policy_id, ada_usd_feed_id, self)
2030

21-
let Transaction { inputs, outputs, .. } = _self
31+
let Transaction { inputs, outputs, mint, extra_signatories, .. } = self
2232

23-
let ada_deposited = get_ada_delta(_policy_id, inputs, outputs)
33+
let minted_amount = get_minted_amount(mint, policy_id)
2434

25-
let Transaction { mint, .. } = _self
26-
let minted_amount = get_minted_amount(mint, _policy_id)
27-
28-
when _redeemer is {
35+
when redeemer is {
2936
Mint -> {
37+
let ada_deposited = get_ada_delta(policy_id, inputs, outputs)
3038
let is_deposit_not_zero: Bool = ada_deposited >= 1
3139

40+
// Collateral requirement: deposited ADA must be worth at least
41+
// collateral_ratio% of the synth tokens being minted.
42+
let collateralized_ada = ada_deposited * collateral_ratio / 100
3243
let expected_minted_amount =
33-
todo @"compute_expected_synth_amount(ada_deposited, raw_price, exponent) — pending lib from partner"
44+
compute_expected_synth_amount(collateralized_ada, raw_price, exponent)
3445

3546
let is_deposit_correct: Bool = minted_amount == expected_minted_amount
3647

@@ -41,34 +52,106 @@ validator synth_dolar(pyth_policy_id: PolicyId, ada_usd_feed_id: Int) {
4152
}
4253

4354
Burn -> {
44-
let is_withdrawal_not_zero: Bool = ada_deposited <= -1
55+
let ada_delta = get_ada_delta(policy_id, inputs, outputs)
56+
let ada_withdrawn = ada_delta * -1
57+
let is_withdrawal_not_zero: Bool = ada_withdrawn >= 1
58+
59+
let expected_burned_amount =
60+
compute_expected_synth_amount(ada_withdrawn, raw_price, exponent)
61+
62+
let is_burn_correct: Bool = minted_amount == -expected_burned_amount
63+
64+
// Derive current debt from pool ADA at oracle price, then compute
65+
// remaining health after the withdrawal to enforce liquidation threshold.
66+
let pool_ada = get_pool_ada(policy_id, inputs)
67+
let current_debt = compute_expected_synth_amount(pool_ada, raw_price, exponent)
68+
let remaining_debt = current_debt + minted_amount
69+
let remaining_ada = pool_ada - ada_withdrawn
70+
let health =
71+
health_ratio(remaining_ada, remaining_debt, raw_price, exponent)
72+
let is_healthy: Bool = health >= liquidation_threshold
73+
74+
// Only the owner can burn — extract owner from pool input datum.
75+
expect [pool_input] =
76+
list.filter(
77+
inputs,
78+
fn(i) { i.output.address.payment_credential == Script(policy_id) },
79+
)
80+
expect InlineDatum(datum_data) = pool_input.output.datum
81+
expect datum: PoolDatum = datum_data
82+
let is_owner_signed: Bool =
83+
list.has(extra_signatories, datum.owner)
84+
85+
and {
86+
is_withdrawal_not_zero,
87+
is_burn_correct,
88+
is_healthy,
89+
is_owner_signed,
90+
}
91+
}
92+
93+
Liquidate -> {
94+
let ada_delta = get_ada_delta(policy_id, inputs, outputs)
95+
let ada_withdrawn = ada_delta * -1
96+
let is_withdrawal_not_zero: Bool = ada_withdrawn >= 1
4597

4698
let expected_burned_amount =
47-
todo @"compute_expected_synth_amount(-ada_deposited, raw_price, exponent) — pending lib from partner"
99+
compute_expected_synth_amount(ada_withdrawn, raw_price, exponent)
48100

49101
let is_burn_correct: Bool = minted_amount == -expected_burned_amount
50102

103+
// Position must be unhealthy for liquidation to be valid — anyone can liquidate.
104+
let pool_ada = get_pool_ada(policy_id, inputs)
105+
let current_debt = compute_expected_synth_amount(pool_ada, raw_price, exponent)
106+
let remaining_debt = current_debt + minted_amount
107+
let remaining_ada = pool_ada - ada_withdrawn
108+
let health =
109+
health_ratio(remaining_ada, remaining_debt, raw_price, exponent)
110+
let is_liquidatable: Bool = health < liquidation_threshold
111+
51112
and {
52113
is_withdrawal_not_zero,
53114
is_burn_correct,
115+
is_liquidatable,
54116
}
55117
}
56118
}
57119
}
58120

59-
spend(_datum: Option<Data>, _redeemer: Action, _utxo: OutputReference, _self: Transaction) {
60-
todo @"spend logic goes here"
121+
spend(_datum: Option<Data>, _redeemer: Action, utxo: OutputReference, self: Transaction) {
122+
// Derive the policy_id from the UTxO being spent — since it's a multi-validator,
123+
// the script hash of the spend address equals the mint policy_id.
124+
let Transaction { inputs, mint, .. } = self
125+
expect Some(own_input) =
126+
list.find(inputs, fn(i) { i.output_reference == utxo })
127+
expect Script(policy_id) = own_input.output.address.payment_credential
128+
129+
// Delegate all validation to the mint policy.
130+
// The spend validator just ensures the mint policy is running in this tx,
131+
// which guarantees price, math, health and owner checks are enforced.
132+
get_minted_amount(mint, policy_id) != 0
61133
}
62134

63135
else(_ctx) {
64136
fail
65137
}
66138
}
67139

140+
/// Returns the lovelace balance of the pool UTxO at the script address.
141+
/// Used to derive current debt and health ratio without tracking state in the datum.
142+
fn get_pool_ada(policy_id: PolicyId, inputs: List<transaction.Input>) -> Int {
143+
expect [pool_input] =
144+
list.filter(
145+
inputs,
146+
fn(i) { i.output.address.payment_credential == Script(policy_id) },
147+
)
148+
lovelace_of(pool_input.output.value)
149+
}
150+
68151
/// Fetches the ADA/USD raw price and exponent from Pyth Lazer.
69152
///
70153
/// Requires the transaction to include:
71-
/// - A Pyth State NFT reference input (identified by `pyth_policy_id`)
154+
/// - A Pyth State NFT reference input (identified by `pythpolicy_id`)
72155
/// - A 0-ADA withdrawal from the Pyth verify script carrying the signed
73156
/// price message as redeemer — verified by that script, not here.
74157
///
@@ -78,13 +161,13 @@ validator synth_dolar(pyth_policy_id: PolicyId, ada_usd_feed_id: Int) {
78161
/// For ADA/USD (feed_id=16, exponent=-8):
79162
/// raw_price = 70_000_000 → $0.70 per ADA
80163
fn get_ada_usd_price(
81-
pyth_policy_id: PolicyId,
164+
pythpolicy_id: PolicyId,
82165
ada_usd_feed_id: Int,
83166
tx: Transaction,
84167
) -> (Int, Int) {
85168
// Pull all price updates from the transaction — the Pyth withdraw script
86169
// has already verified the Ed25519 signature by the time we get here.
87-
let updates = pyth.get_updates(pyth_policy_id, tx)
170+
let updates = pyth.get_updates(pythpolicy_id, tx)
88171

89172
// We expect exactly one price update message per transaction.
90173
expect [update] = updates

0 commit comments

Comments
 (0)