11use aiken/ collection/ dict
2+ use utils.{compute_expected_synth_amount, health_ratio}
23use aiken/ collection/ list
34use cardano/ address.{Script }
45use cardano/ assets.{PolicyId , Value , lovelace_of, tokens}
5- use cardano/ transaction.{Transaction , OutputReference }
6+ use cardano/ transaction.{Transaction , OutputReference , InlineDatum }
67use pyth
78use pyth.{PriceUpdate }
89use types/ u32
910
11+ pub type PoolDatum {
12+ owner: ByteArray
13+ }
14+
1015pub 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
80163fn 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