partner_chains_cardano_offchain/reserve/
release.rs

1#![allow(rustdoc::private_intra_doc_links)]
2// This transaction releases some funds to the illiquid supply.
3//! Inputs:
4//!     - previous utxo at the *reserve validator*, containing the reserve tokens and the
5//!       [ReserveData] plutus data with reserve configuration and release stats
6//! Reference inputs:
7//!     - utxo with V-Function reference script matching the hash saved in the input [ReserveData].
8//!       IMPORTANT: The V-Function script will evaluate against the total number of tokens that
9//!                  should have been released up to now.
10//!                  The number of tokens released in a single transaction equals
11//!                  `<current v-function value> - <number of previously released tokens>`.
12//!     - utxo with authentication policy reference script
13//!     - utxo with validator version policy reference script
14//!     - utxo with illiquid supply validator reference script
15//! Outputs:
16//!     - utxo at the *reserve validator* containing the rest of unreleased tokens and the
17//!       updated [ReserveData] plutus data
18//!     - utxo at the *illiquid supply validator* containing the newly released tokens
19//! Mints:
20//!     - V-Function tokens in the number equal to *the total number of reserve tokens released
21//!       including the ones released in this transaction*. Ie. if N tokens were already released
22//!       and M tokens are being released, the transaction should mint N+M V-Function tokens.
23//!       These tokens are worthless and don't serve any purpose after the transaction is done.
24use super::{
25	ReserveData, add_ics_utxo_input_with_validator_script_reference,
26	add_reserve_utxo_input_with_validator_script_reference,
27};
28use crate::{
29	await_tx::AwaitTx, cardano_keys::CardanoPaymentSigningKey, csl::*, plutus_script::PlutusScript,
30	reserve::ReserveUtxo,
31};
32use anyhow::anyhow;
33use cardano_serialization_lib::{
34	Assets, Int, MultiAsset, PlutusData, Transaction, TransactionBuilder, TransactionOutputBuilder,
35	TxInputsBuilder,
36};
37use ogmios_client::{
38	query_ledger_state::*, query_network::QueryNetwork, transactions::Transactions,
39	types::OgmiosUtxo,
40};
41use partner_chains_plutus_data::{bridge::TokenTransferDatumV1, reserve::ReserveRedeemer};
42use sidechain_domain::{McTxHash, UtxoId};
43use std::num::NonZero;
44
45/// Releases funds from reserve.
46pub async fn release_reserve_funds<
47	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
48	A: AwaitTx,
49>(
50	amount: NonZero<u64>,
51	genesis_utxo: UtxoId,
52	reference_utxo: UtxoId,
53	payment_key: &CardanoPaymentSigningKey,
54	client: &T,
55	await_tx: &A,
56) -> anyhow::Result<McTxHash> {
57	let ctx = TransactionContext::for_payment_key(payment_key, client).await?;
58	let tip = client.get_tip().await?;
59	let reserve_data = ReserveData::get(genesis_utxo, &ctx, client).await?;
60	let Some(reference_utxo) = client.query_utxo_by_id(reference_utxo).await? else {
61		return Err(anyhow!("Reference utxo {reference_utxo:?} not found on chain"));
62	};
63
64	let reserve_utxo = reserve_data.get_reserve_utxo(&ctx, client).await?;
65	let ics_utxo = reserve_data.get_illiquid_circulation_supply_utxo(&ctx, client).await?;
66
67	let tx = Costs::calculate_costs(
68		|costs| {
69			reserve_release_tx(
70				&ctx,
71				&reserve_data,
72				&reserve_utxo,
73				&ics_utxo,
74				&reference_utxo,
75				amount.into(),
76				tip.slot,
77				costs,
78			)
79		},
80		client,
81	)
82	.await?;
83
84	let signed_tx = ctx.sign(&tx).to_bytes();
85
86	let res = client.submit_transaction(&signed_tx).await.map_err(|e| {
87		anyhow::anyhow!(
88			"Reserve release transaction request failed: {}, tx bytes: {}",
89			e,
90			hex::encode(signed_tx)
91		)
92	})?;
93	let tx_id = res.transaction.id;
94	log::info!("Reserve release transaction submitted: {}", hex::encode(tx_id));
95	await_tx.await_tx_output(client, McTxHash(tx_id)).await?;
96
97	Ok(McTxHash(tx_id))
98}
99
100fn reserve_release_tx(
101	ctx: &TransactionContext,
102	reserve_data: &ReserveData,
103	previous_reserve: &ReserveUtxo,
104	ics_utxo: &OgmiosUtxo,
105	reference_utxo: &OgmiosUtxo,
106	amount_to_transfer: u64,
107	latest_slot: u64,
108	costs: Costs,
109) -> anyhow::Result<Transaction> {
110	let token = &previous_reserve.datum.immutable_settings.token;
111	let stats = &previous_reserve.datum.stats;
112
113	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
114
115	let reserve_balance = previous_reserve.utxo.get_asset_amount(token);
116	let ics_balance = ics_utxo.get_asset_amount(token);
117	let token_total_amount_transferred = stats.token_total_amount_transferred;
118	let cumulative_total_transfer: u64 = token_total_amount_transferred
119		.checked_add(amount_to_transfer)
120		.expect("cumulative_total_transfer can't overflow u64");
121
122	let left_in_reserve = reserve_balance.checked_sub(amount_to_transfer)
123		.ok_or_else(||anyhow!("Not enough funds in the reserve to transfer {amount_to_transfer} tokens (reserve balance: {reserve_balance})"))?;
124
125	// Additional reference scripts
126	tx_builder.add_script_reference_input(
127		&reserve_data.auth_policy_version_utxo.to_csl_tx_input(),
128		reserve_data.scripts.auth_policy.bytes.len(),
129	);
130	tx_builder.add_script_reference_input(
131		&reserve_data
132			.illiquid_circulation_supply_validator_version_utxo
133			.to_csl_tx_input(),
134		reserve_data.scripts.illiquid_circulation_supply_validator.bytes.len(),
135	);
136	tx_builder.add_script_reference_input(
137		&reserve_data
138			.illiquid_circulation_supply_authority_token_policy_version_utxo
139			.to_csl_tx_input(),
140		reserve_data.scripts.illiquid_circulation_supply_auth_token_policy.bytes.len(),
141	);
142
143	// Mint v-function tokens in the number equal to the *total* number of tokens transferred.
144	// This serves as a validation of the v-function value.
145	let v_function = v_function_from_utxo(reference_utxo)?;
146	tx_builder.add_mint_script_token_using_reference_script(
147		&Script::Plutus(v_function),
148		&reference_utxo.to_csl_tx_input(),
149		&Int::new(&cumulative_total_transfer.into()),
150		&costs,
151	)?;
152
153	let mut tx_inputs = TxInputsBuilder::new();
154
155	let spend_indices = costs.get_spend_indices();
156
157	let mut spend_costs = vec![
158		costs.get_spend(*spend_indices.get(1).unwrap_or(&0)),
159		costs.get_spend(*spend_indices.get(0).unwrap_or(&0)),
160	];
161
162	spend_costs.sort();
163
164	// Remove tokens from the reserve
165	add_reserve_utxo_input_with_validator_script_reference(
166		&mut tx_inputs,
167		&previous_reserve.utxo,
168		reserve_data,
169		ReserveRedeemer::ReleaseFromReserve,
170		spend_costs.get(1).unwrap(),
171	)?;
172
173	add_ics_utxo_input_with_validator_script_reference(
174		&mut tx_inputs,
175		ics_utxo,
176		reserve_data,
177		spend_costs.get(0).unwrap(),
178	)?;
179
180	tx_builder.set_inputs(&tx_inputs);
181
182	let mut ics_tokens = ics_utxo
183		.value
184		.to_csl()?
185		.multiasset()
186		.expect("ICS UTXO should have native tokens");
187
188	let mut assets = Assets::new();
189
190	assets.insert(
191		&token.asset_name.to_csl()?,
192		&amount_to_transfer.saturating_add(ics_balance).into(),
193	);
194
195	ics_tokens.insert(&token.policy_id.0.into(), &assets);
196
197	// Transfer released tokens to the illiquid supply
198	tx_builder.add_output(&{
199		TransactionOutputBuilder::new()
200			.with_address(
201				&reserve_data.scripts.illiquid_circulation_supply_validator.address(ctx.network),
202			)
203			.with_plutus_data(&TokenTransferDatumV1::ReserveTransfer.into())
204			.next()?
205			.with_minimum_ada_and_asset(&ics_tokens, ctx)?
206			.build()?
207	})?;
208
209	// Return the rest of the tokens back to the reserve
210	tx_builder.add_output(&{
211		TransactionOutputBuilder::new()
212			.with_address(&reserve_data.scripts.validator.address(ctx.network))
213			.with_plutus_data(&PlutusData::from(
214				previous_reserve.datum.clone().after_withdrawal(amount_to_transfer),
215			))
216			.next()?
217			.with_minimum_ada_and_asset(
218				&MultiAsset::from_ogmios_utxo(&previous_reserve.utxo)?
219					.with_asset_amount(token, left_in_reserve)?,
220				ctx,
221			)?
222			.build()?
223	})?;
224
225	tx_builder.set_validity_start_interval_bignum(latest_slot.into());
226
227	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
228}
229
230fn v_function_from_utxo(utxo: &OgmiosUtxo) -> anyhow::Result<PlutusScript> {
231	let Some(v_function_script) = utxo.script.clone() else {
232		return Err(anyhow!("V-Function reference script missing from the reference UTXO",));
233	};
234	PlutusScript::try_from(v_function_script)
235		.map_err(|val| anyhow!("{val:?} is not a valid Plutus Script"))
236}
237
238#[cfg(test)]
239mod tests {
240	use super::{AssetNameExt, Costs, TransactionContext, empty_asset_name, reserve_release_tx};
241	use crate::{
242		cardano_keys::CardanoPaymentSigningKey,
243		plutus_script,
244		plutus_script::PlutusScript,
245		reserve::{ReserveData, ReserveUtxo, release::OgmiosUtxoExt},
246		scripts_data::ReserveScripts,
247		test_values::{payment_addr, protocol_parameters},
248	};
249	use cardano_serialization_lib::{Int, NetworkIdKind, PolicyID, Transaction};
250	use hex_literal::hex;
251	use ogmios_client::types::{Asset, OgmiosTx, OgmiosUtxo, OgmiosValue};
252	use partner_chains_plutus_data::reserve::{
253		ReserveDatum, ReserveImmutableSettings, ReserveMutableSettings, ReserveStats,
254	};
255	use pretty_assertions::assert_eq;
256	use raw_scripts::{
257		EXAMPLE_V_FUNCTION_POLICY, ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY,
258		ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR, RESERVE_AUTH_POLICY, RESERVE_VALIDATOR,
259	};
260	use sidechain_domain::{AssetName, PolicyId};
261
262	fn payment_key() -> CardanoPaymentSigningKey {
263		CardanoPaymentSigningKey::from_normal_bytes(hex!(
264			"94f7531c9639654b77fa7e10650702b6937e05cd868f419f54bcb8368e413f04"
265		))
266		.unwrap()
267	}
268
269	fn test_address_bech32() -> String {
270		"addr_test1vpmd59ajuvm34d723r8q2qzyz9ylq0x9pygqn7vun8qgpkgs7y5hw".into()
271	}
272
273	fn payment_utxo() -> OgmiosUtxo {
274		OgmiosUtxo {
275			transaction: OgmiosTx {
276				id: hex!("f5e751f474e909419c714bb5666a8f810e7ed61fadad236c29f67dafc1ff398b"),
277			},
278			index: 1,
279			value: OgmiosValue {
280				lovelace: 994916563,
281				native_tokens: [(
282					// random native token
283					hex!("08b95138e16a062fa8d623a2b1beebd59c06210f3d33690580733e73"),
284					vec![Asset { name: vec![], amount: 1 }],
285				)]
286				.into(),
287			},
288			address: test_address_bech32(),
289
290			..OgmiosUtxo::default()
291		}
292	}
293
294	fn tx_context() -> TransactionContext {
295		TransactionContext {
296			payment_key: payment_key(),
297			payment_key_utxos: vec![payment_utxo()],
298			network: NetworkIdKind::Testnet,
299			protocol_parameters: protocol_parameters(),
300			change_address: payment_addr(),
301		}
302	}
303
304	fn reserve_validator_script() -> PlutusScript {
305		RESERVE_VALIDATOR.into()
306	}
307
308	fn auth_policy_script() -> PlutusScript {
309		RESERVE_AUTH_POLICY.into()
310	}
311
312	fn illiquid_supply_validator_script() -> PlutusScript {
313		ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR.into()
314	}
315
316	fn illiquid_supply_auth_token_policy_script() -> PlutusScript {
317		ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY.into()
318	}
319
320	const UNIX_T0: u64 = 1736504093000u64;
321
322	fn applied_v_function() -> PlutusScript {
323		plutus_script![EXAMPLE_V_FUNCTION_POLICY, UNIX_T0].unwrap()
324	}
325
326	fn version_oracle_address() -> String {
327		"addr_test1wqskkgpmsyf0yr2renk0spgsvea75rkq4yvalrpwwudwr5ga3relp".to_string()
328	}
329
330	fn ics_validator_address() -> String {
331		"addr_test1wz7s8uzkldpv7rvyu6v8wg4vk3ulxy23kzqyngk2yc76xucestz8f".to_string()
332	}
333
334	fn reserve_data() -> ReserveData {
335		ReserveData {
336			scripts: ReserveScripts {
337				validator: reserve_validator_script(),
338				auth_policy: auth_policy_script(),
339				illiquid_circulation_supply_validator: illiquid_supply_validator_script(),
340				illiquid_circulation_supply_auth_token_policy:
341					illiquid_supply_auth_token_policy_script(),
342			},
343			auth_policy_version_utxo: OgmiosUtxo {
344				transaction: OgmiosTx {
345					id: hex!("d1030b0ce5cf33d97a6e8aafa1cfe150e7a8b3a5584bd7a345743938e78ec44b"),
346				},
347				index: 0,
348				address: version_oracle_address(),
349				script: Some(auth_policy_script().into()),
350				..Default::default()
351			},
352			validator_version_utxo: OgmiosUtxo {
353				transaction: OgmiosTx {
354					id: hex!("fcb5d7877e6ce7cfaef579c4f4b5fdbbdb807e4fe613752671742f1a5191c850"),
355				},
356				index: 0,
357				address: version_oracle_address(),
358				script: Some(reserve_validator_script().into()),
359				..Default::default()
360			},
361			illiquid_circulation_supply_validator_version_utxo: OgmiosUtxo {
362				transaction: OgmiosTx {
363					id: hex!("f5890475177fcc7cf40679974751f66331c7b25fcf2f1a148c53cf7e0e147114"),
364				},
365				index: 0,
366				address: version_oracle_address(),
367				script: Some(illiquid_supply_validator_script().into()),
368				..Default::default()
369			},
370			illiquid_circulation_supply_authority_token_policy_version_utxo: OgmiosUtxo {
371				transaction: OgmiosTx {
372					id: hex!("f5890475177fcc7cf40679974751f66331c7b25fcf2f1a148c53cf7e0e147114"),
373				},
374				index: 1,
375				address: version_oracle_address(),
376				script: Some(illiquid_supply_auth_token_policy_script().into()),
377				..Default::default()
378			},
379		}
380	}
381
382	fn reference_utxo() -> OgmiosUtxo {
383		OgmiosUtxo {
384			transaction: OgmiosTx {
385				id: hex!("45882cfd2de9381f34ae68ad073452e2a57a7ad11095dae49f365266637e9d04"),
386			},
387			index: 0,
388			script: Some(applied_v_function().into()),
389			..Default::default()
390		}
391	}
392
393	fn previous_reserve_ogmios_utxo() -> OgmiosUtxo {
394		OgmiosUtxo {
395			transaction: OgmiosTx {
396				id: hex!("23de508bbfeb6af651da305a2de022463f71e47d58365eba36d98fa6c4aed731"),
397			},
398			value: OgmiosValue {
399				lovelace: 1672280,
400				native_tokens: [
401					(
402						// reserve token
403						token_policy().0,
404						vec![Asset { name: token_name().0.to_vec(), amount: 990 }],
405					),
406					(
407						// leftover governance token - should be returned to the validator
408						hex!("75b8875ff8958c66fecbd93740ac5ffd7370d299e729a46bb5632066"),
409						vec![Asset { name: vec![], amount: 1 }],
410					),
411				]
412				.into(),
413			},
414
415			index: 1,
416			..Default::default()
417		}
418	}
419
420	fn previous_reserve_utxo() -> ReserveUtxo {
421		ReserveUtxo { utxo: previous_reserve_ogmios_utxo(), datum: previous_reserve_datum() }
422	}
423
424	fn ics_utxo() -> OgmiosUtxo {
425		OgmiosUtxo {
426			transaction: OgmiosTx {
427				id: hex!("23de508bbfeb6af651da305a2de022463f71e47d58365eba36d98fa6c4aed731"),
428			},
429			value: OgmiosValue {
430				lovelace: 1672280,
431				native_tokens: [(
432					// reserve token
433					ics_auth_token_policy().0,
434					vec![Asset { name: vec![], amount: 1 }],
435				)]
436				.into(),
437			},
438
439			index: 2,
440			address: ics_validator_address(),
441			..Default::default()
442		}
443	}
444
445	fn token_policy() -> PolicyId {
446		PolicyId(hex!("1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2b"))
447	}
448
449	fn ics_auth_token_policy() -> PolicyId {
450		PolicyId(hex!("d4a1d93484fa63847f8c4c271dc0c46a55b6e5916f46e14ee849a381"))
451	}
452
453	fn token_name() -> AssetName {
454		AssetName::from_hex_unsafe("52657761726420746f6b656e")
455	}
456
457	fn token_id() -> sidechain_domain::AssetId {
458		sidechain_domain::AssetId { policy_id: token_policy(), asset_name: token_name() }
459	}
460
461	fn previous_reserve_datum() -> ReserveDatum {
462		ReserveDatum {
463			immutable_settings: ReserveImmutableSettings { token: token_id() },
464			mutable_settings: ReserveMutableSettings {
465				total_accrued_function_asset_name: applied_v_function().policy_id(),
466				initial_incentive: 0,
467			},
468			stats: ReserveStats { token_total_amount_transferred: 10 },
469		}
470	}
471
472	fn reserve_release_test_tx() -> Transaction {
473		reserve_release_tx(
474			&tx_context(),
475			&reserve_data(),
476			&previous_reserve_utxo(),
477			&ics_utxo(),
478			&reference_utxo(),
479			5,
480			0,
481			Costs::ZeroCosts,
482		)
483		.unwrap()
484	}
485
486	#[test]
487	fn should_have_correct_reference_utxos() {
488		let ref_inputs: Vec<_> = reserve_release_test_tx()
489			.body()
490			.reference_inputs()
491			.expect("Should have reference inputs")
492			.into_iter()
493			.cloned()
494			.collect();
495
496		assert!(ref_inputs.contains(&reference_utxo().to_csl_tx_input()));
497		assert!(ref_inputs.contains(&reserve_data().auth_policy_version_utxo.to_csl_tx_input()));
498		assert!(ref_inputs.contains(&reserve_data().validator_version_utxo.to_csl_tx_input()));
499		assert!(
500			ref_inputs.contains(
501				&reserve_data()
502					.illiquid_circulation_supply_authority_token_policy_version_utxo
503					.to_csl_tx_input()
504			)
505		);
506		assert!(
507			ref_inputs.contains(
508				&reserve_data()
509					.illiquid_circulation_supply_validator_version_utxo
510					.to_csl_tx_input()
511			)
512		);
513		assert_eq!(ref_inputs.len(), 5)
514	}
515
516	#[test]
517	fn should_mint_v_function_scripts() {
518		let v_function_token_mint_amount = reserve_release_test_tx()
519			.body()
520			.mint()
521			.expect("Should mint a token")
522			.get(&applied_v_function().csl_script_hash())
523			.and_then(|policy| policy.get(0))
524			.expect("Should mint a v-function policy token")
525			.get(&empty_asset_name())
526			.expect("The minted token should have an empty asset name");
527
528		assert_eq!(v_function_token_mint_amount, Int::new_i32(15))
529	}
530
531	#[test]
532	fn should_burn_previoius_reserve_utxo() {
533		let inputs: Vec<_> =
534			reserve_release_test_tx().body().inputs().into_iter().cloned().collect();
535
536		assert!(inputs.contains(&previous_reserve_utxo().utxo.to_csl_tx_input()))
537	}
538
539	#[test]
540	fn should_add_correct_number_of_tokens_to_illiquid_supply() {
541		let illiquid_supply_output = (reserve_release_test_tx().body().outputs().into_iter())
542			.find(|output| {
543				output.address()
544					== illiquid_supply_validator_script().address(NetworkIdKind::Testnet)
545			})
546			.expect("Should output a UTXO to illiquid supply validator")
547			.amount()
548			.multiasset()
549			.expect("Should output native tokens to illiquid supply")
550			.get(&PolicyID::from(token_policy().0))
551			.expect("Should transfer reserve token policy token to illiquid supply")
552			.get(&token_name().to_csl().unwrap())
553			.expect("Should transfer reserve token to illiquid supply");
554
555		assert_eq!(illiquid_supply_output, 5u64.into())
556	}
557
558	#[test]
559	fn should_leave_unreleased_tokens_at_reserve_validator() {
560		let validator_output = (reserve_release_test_tx().body().outputs().into_iter())
561			.find(|output| {
562				output.address() == reserve_validator_script().address(NetworkIdKind::Testnet)
563			})
564			.expect("Should output a UTXO to illiquid supply validator")
565			.amount()
566			.multiasset()
567			.expect("Should output native tokens to illiquid supply")
568			.get(&PolicyID::from(token_policy().0))
569			.expect("Should transfer reserve token policy token to illiquid supply")
570			.get(&token_name().to_csl().unwrap())
571			.expect("Should transfer reserve token to illiquid supply");
572
573		assert_eq!(validator_output, 985u64.into())
574	}
575}