partner_chains_cardano_offchain/reserve/
init.rs

1//! Initialization of the reserve management is execution of three similar transaction to
2//! initialize three scripts: Rerserve Management Validator, Reserve Management Policy and
3//! Illiquid Circulation Supply Validator.
4//!
5//! Transaction for each of these scripts should have:
6//! * an output to Version Oracle Validator address that should:
7//! * * have script reference with the script being initialized attached, script should be applied with Version Oracle Policy Id
8//! * * contain 1 token of Version Oracle Policy with "Version oracle" asset name, minted in this transaction
9//! * * * mint redeemer should be Constr(1, [Int: SCRIPT_ID, Bytes: Applied Script Hash])
10//! * * have Plutus Data that is [Int: SCRIPT_ID, Bytes: Version Oracle Policy Id]
11//! * an output to the current governance (holder of governance token) that should:
12//! * * contain a new Goveranance Policy token, minted in this transaction,
13//! * * * mint redeemer should be empty contructor Plutus Data
14//! * a script reference input of the current Goveranance UTXO
15//! * signature of the current goveranance
16
17use crate::plutus_script;
18use crate::{
19	await_tx::AwaitTx,
20	cardano_keys::CardanoPaymentSigningKey,
21	csl::{
22		CostStore, Costs, MultiAssetExt, OgmiosUtxoExt, TransactionBuilderExt, TransactionContext,
23		TransactionExt, TransactionOutputAmountBuilderExt, get_builder_config,
24	},
25	governance::GovernanceData,
26	multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign},
27	plutus_script::PlutusScript,
28	scripts_data::{self, PlutusScriptData},
29};
30use cardano_serialization_lib::{
31	AssetName, BigNum, ConstrPlutusData, JsError, MultiAsset, PlutusData, PlutusList, ScriptRef,
32	Transaction, TransactionBuilder, TransactionOutputBuilder,
33};
34use ogmios_client::{
35	query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
36	query_network::QueryNetwork,
37	transactions::Transactions,
38	types::OgmiosUtxo,
39};
40use partner_chains_plutus_data::version_oracle::VersionOracleDatum;
41use raw_scripts::{
42	ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY, ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR,
43	RESERVE_AUTH_POLICY, RESERVE_VALIDATOR, ScriptId,
44};
45use sidechain_domain::UtxoId;
46
47/// Stores smart contracts used for reserve management in the versioning system.
48/// Scripts stored are:
49///  - Reserve Management Validator
50///  - Reserve Management Policy
51///  - Illiquid Circulation Supply Validator
52pub async fn init_reserve_management<
53	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
54	A: AwaitTx,
55>(
56	genesis_utxo: UtxoId,
57	payment_key: &CardanoPaymentSigningKey,
58	client: &T,
59	await_tx: &A,
60) -> anyhow::Result<Vec<MultiSigSmartContractResult>> {
61	let reserve_validator = ScriptData::new(
62		"Reserve Management Validator",
63		RESERVE_VALIDATOR.0.to_vec(),
64		ScriptId::ReserveValidator,
65	);
66	let reserve_policy = ScriptData::new(
67		"Reserve Management Policy",
68		RESERVE_AUTH_POLICY.0.to_vec(),
69		ScriptId::ReserveAuthPolicy,
70	);
71	let ics_validator = ScriptData::new(
72		"Illiquid Circulation Supply Validator",
73		ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR.0.to_vec(),
74		ScriptId::IlliquidCirculationSupplyValidator,
75	);
76	let ics_auth_token_policy = ScriptData::new(
77		"Illiquid Circulation Supply Auth Token Policy",
78		plutus_script![
79			ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY,
80			ScriptId::IlliquidCirculationSupplyAuthorityTokenPolicy
81		]?
82		.bytes
83		.to_vec(),
84		ScriptId::IlliquidCirculationSupplyAuthorityTokenPolicy,
85	);
86
87	Ok(vec![
88		initialize_script(reserve_validator, genesis_utxo, payment_key, client, await_tx).await?,
89		initialize_script(reserve_policy, genesis_utxo, payment_key, client, await_tx).await?,
90		initialize_script(ics_validator, genesis_utxo, payment_key, client, await_tx).await?,
91		initialize_script(ics_auth_token_policy, genesis_utxo, payment_key, client, await_tx)
92			.await?,
93	]
94	.into_iter()
95	.flatten()
96	.collect())
97}
98
99struct ScriptData {
100	name: String,
101	plutus_script: PlutusScript,
102	id: u32,
103}
104
105impl ScriptData {
106	fn new(name: &str, raw_bytes: Vec<u8>, id: ScriptId) -> Self {
107		let plutus_script =
108			PlutusScript::v2_from_cbor(&raw_bytes).expect("Plutus script should be valid");
109		Self { name: name.to_string(), plutus_script, id: id as u32 }
110	}
111
112	fn applied_plutus_script(
113		&self,
114		version_oracle: &PlutusScriptData,
115	) -> Result<PlutusScript, JsError> {
116		self.plutus_script
117			.clone()
118			.apply_data_uplc(version_oracle.policy_id_as_plutus_data())
119			.map_err(|e| JsError::from_str(&e.to_string()))
120	}
121}
122
123async fn initialize_script<
124	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
125	A: AwaitTx,
126>(
127	script: ScriptData,
128	genesis_utxo: UtxoId,
129	payment_key: &CardanoPaymentSigningKey,
130	client: &T,
131	await_tx: &A,
132) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
133	let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
134	let governance = GovernanceData::get(genesis_utxo, client).await?;
135	let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
136
137	if script_is_initialized(&script, &version_oracle, &payment_ctx, client).await? {
138		log::info!("Script '{}' is already initialized", script.name);
139		return Ok(None);
140	}
141	Ok(Some(
142		submit_or_create_tx_to_sign(
143			&governance,
144			payment_ctx,
145			|costs, ctx| init_script_tx(&script, &governance, &version_oracle, costs, &ctx),
146			"Init Reserve Management",
147			client,
148			await_tx,
149		)
150		.await?,
151	))
152}
153
154fn init_script_tx(
155	script: &ScriptData,
156	governance: &GovernanceData,
157	version_oracle: &PlutusScriptData,
158	costs: Costs,
159	ctx: &TransactionContext,
160) -> anyhow::Result<Transaction> {
161	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
162	let applied_script = script.applied_plutus_script(version_oracle)?;
163	{
164		let witness = PlutusData::new_constr_plutus_data(&ConstrPlutusData::new(
165			&BigNum::one(),
166			&version_oracle_plutus_list(script.id, &applied_script.script_hash()),
167		));
168		tx_builder.add_mint_one_script_token(
169			&version_oracle.policy,
170			&version_oracle_asset_name(),
171			&witness,
172			&costs.get_mint(&version_oracle.policy.clone()),
173		)?;
174	}
175	{
176		let script_ref = ScriptRef::new_plutus_script(&applied_script.to_csl());
177		let amount_builder = TransactionOutputBuilder::new()
178			.with_address(&version_oracle.validator.address(ctx.network))
179			.with_plutus_data(&PlutusData::new_list(&version_oracle_plutus_list(
180				script.id,
181				&version_oracle.policy.script_hash(),
182			)))
183			.with_script_ref(&script_ref)
184			.next()?;
185		let ma = MultiAsset::new()
186			.with_asset_amount(&version_oracle.policy.asset(version_oracle_asset_name())?, 1u64)?;
187		let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
188		tx_builder.add_output(&output)?;
189	}
190	// Mint governance token
191	let gov_tx_input = governance.utxo_id_as_tx_input();
192	tx_builder.add_mint_one_script_token_using_reference_script(
193		&governance.policy.script(),
194		&gov_tx_input,
195		&costs,
196	)?;
197	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
198}
199
200fn version_oracle_asset_name() -> AssetName {
201	AssetName::new(b"Version oracle".to_vec()).unwrap()
202}
203
204fn version_oracle_plutus_list(script_id: u32, script_hash: &[u8]) -> PlutusList {
205	let mut list = PlutusList::new();
206	list.add(&PlutusData::new_integer(&script_id.into()));
207	list.add(&PlutusData::new_bytes(script_hash.to_vec()));
208	list
209}
210
211// There exist UTXO at Version Oracle Validator with Datum that contains
212// * script id of the script being initialized
213// * Version Oracle Policy Id
214async fn script_is_initialized<T: QueryLedgerState>(
215	script: &ScriptData,
216	version_oracle: &PlutusScriptData,
217	ctx: &TransactionContext,
218	client: &T,
219) -> Result<bool, anyhow::Error> {
220	Ok(find_script_utxo(script.id, version_oracle, ctx, client).await?.is_some())
221}
222
223/// Finds an UTXO at Version Oracle Validator with Datum that contains
224/// * given script id
225/// * Version Oracle Policy Id
226pub(crate) async fn find_script_utxo<T: QueryLedgerState>(
227	script_id: u32,
228	version_oracle: &PlutusScriptData,
229	ctx: &TransactionContext,
230	client: &T,
231) -> Result<Option<OgmiosUtxo>, anyhow::Error> {
232	let validator_address = version_oracle.validator.address(ctx.network).to_bech32(None)?;
233	let validator_utxos = client.query_utxos(&[validator_address]).await?;
234	// Decode datum from utxos and check if it contains script id
235	Ok(validator_utxos.into_iter().find(|utxo| {
236		utxo.get_plutus_data()
237			.and_then(|data| TryInto::<VersionOracleDatum>::try_into(data).ok())
238			.is_some_and(|datum| {
239				datum.version_oracle == script_id
240					&& datum.currency_symbol == version_oracle.policy.script_hash()
241			})
242	}))
243}
244
245#[cfg(test)]
246mod tests {
247	use super::{ScriptData, init_script_tx};
248	use crate::{
249		csl::{Costs, OgmiosUtxoExt, TransactionContext, unit_plutus_data},
250		governance::GovernanceData,
251		scripts_data::{self, PlutusScriptData},
252		test_values::{
253			make_utxo, payment_addr, payment_key, protocol_parameters, test_governance_policy,
254		},
255	};
256	use cardano_serialization_lib::{
257		AssetName, BigNum, ConstrPlutusData, ExUnits, Int, NetworkIdKind, PlutusData, PlutusList,
258		RedeemerTag, ScriptHash, Transaction,
259	};
260	use ogmios_client::types::{OgmiosTx, OgmiosUtxo};
261	use raw_scripts::ScriptId;
262	use sidechain_domain::UtxoId;
263
264	#[test]
265	fn init_script_tx_version_oracle_output_test() {
266		let outputs = make_init_script_tx().body().outputs();
267		let voo = outputs
268			.into_iter()
269			.find(|o| o.address() == version_oracle_validator_address())
270			.expect("Init Script Transaction should have output to Version Oracle Validator");
271		let voo_script_ref = voo
272			.script_ref()
273			.expect("Version Oracle Validator output should have script reference");
274		let voo_plutus_script = voo_script_ref
275			.plutus_script()
276			.expect("Script reference should be Plutus script");
277		assert_eq!(
278			voo_plutus_script,
279			test_initialized_script()
280				.applied_plutus_script(&version_oracle_data())
281				.unwrap()
282				.to_csl()
283		);
284		let amount = voo
285			.amount()
286			.multiasset()
287			.and_then(|ma| ma.get(&version_oracle_policy_csl_script_hash()))
288			.and_then(|vo_ma| vo_ma.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
289			.expect("Version Oracle Validator output has a token of Version Oracle Policy");
290
291		assert_eq!(amount, 1u64.into());
292
293		let voo_plutus_data = voo
294			.plutus_data()
295			.and_then(|pd| pd.as_list())
296			.expect("Version Oracle Validator output should have Plutus Data of List type");
297		assert_eq!(
298			voo_plutus_data.get(0),
299			PlutusData::new_integer(&test_initialized_script().id.into())
300		);
301		assert_eq!(
302			voo_plutus_data.get(1),
303			PlutusData::new_bytes(version_oracle_data().policy.script_hash().to_vec())
304		);
305	}
306
307	#[test]
308	fn init_script_tx_change_output_test() {
309		let outputs = make_init_script_tx().body().outputs();
310		let change_output = outputs
311			.into_iter()
312			.find(|o| o.address() == payment_addr())
313			.expect("Change output has to be present to keep governance token")
314			.clone();
315		let gov_token_amount = change_output
316			.amount()
317			.multiasset()
318			.and_then(|ma| ma.get(&test_governance_data().policy.script().script_hash().into()))
319			.and_then(|gov_ma| gov_ma.get(&AssetName::new(vec![]).unwrap()))
320			.unwrap();
321		assert_eq!(gov_token_amount, BigNum::one(), "Change contains one governance token");
322	}
323
324	#[test]
325	fn init_script_tx_reference_input() {
326		// a script reference input of the current Governance UTXO
327		let ref_input = make_init_script_tx()
328			.body()
329			.reference_inputs()
330			.expect("Init transaction should have reference input")
331			.get(0);
332		assert_eq!(ref_input, test_governance_input().to_csl_tx_input());
333	}
334
335	#[test]
336	fn init_script_tx_mints() {
337		let tx = make_init_script_tx();
338		let vo_mint = tx
339			.body()
340			.mint()
341			.and_then(|mint| mint.get(&version_oracle_policy_csl_script_hash()))
342			.and_then(|assets| assets.get(0))
343			.and_then(|assets| assets.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
344			.expect("Transaction should have a mint of Version Oracle Policy token");
345		assert_eq!(vo_mint, Int::new_i32(1));
346
347		let gov_mint = tx
348			.body()
349			.mint()
350			.and_then(|mint| mint.get(&test_governance_data().policy.script().script_hash().into()))
351			.and_then(|assets| assets.get(0))
352			.and_then(|assets| assets.get(&AssetName::new(vec![]).unwrap()))
353			.expect("Transaction should have a mint of Governance Policy token");
354		assert_eq!(gov_mint, Int::new_i32(1));
355	}
356
357	#[test]
358	fn init_script_tx_redeemers() {
359		// This is so cumbersome, because of the CSL interface for Redeemers
360		let ws = make_init_script_tx()
361			.witness_set()
362			.redeemers()
363			.expect("Transaction has two redeemers");
364		let redeemers = vec![ws.get(0), ws.get(1)];
365
366		let expected_vo_redeemer_data = {
367			let mut list = PlutusList::new();
368			let script_id: u64 = test_initialized_script().id.into();
369			list.add(&PlutusData::new_integer(&script_id.into()));
370			list.add(&PlutusData::new_bytes(
371				test_initialized_script()
372					.applied_plutus_script(&version_oracle_data())
373					.unwrap()
374					.script_hash()
375					.to_vec(),
376			));
377			let constr_plutus_data = ConstrPlutusData::new(&BigNum::one(), &list);
378			PlutusData::new_constr_plutus_data(&constr_plutus_data)
379		};
380
381		let _ = redeemers
382			.iter()
383			.find(|r| {
384				r.tag() == RedeemerTag::new_mint()
385					&& r.data() == expected_vo_redeemer_data
386					&& r.ex_units() == versioning_script_cost()
387			})
388			.expect("Transaction should have a mint redeemer for Version Oracle Policy");
389		let _ = redeemers
390			.iter()
391			.find(|r| {
392				r.tag() == RedeemerTag::new_mint()
393					&& r.data() == unit_plutus_data()
394					&& r.ex_units() == governance_script_cost()
395			})
396			.expect("Transaction should have a mint redeemer for Governance Policy");
397	}
398
399	fn make_init_script_tx() -> Transaction {
400		init_script_tx(
401			&test_initialized_script(),
402			&test_governance_data(),
403			&version_oracle_data(),
404			test_costs(),
405			&test_transaction_context(),
406		)
407		.unwrap()
408	}
409
410	fn test_initialized_script() -> ScriptData {
411		ScriptData::new(
412			"Test Script",
413			raw_scripts::RESERVE_VALIDATOR.0.to_vec(),
414			ScriptId::ReserveValidator,
415		)
416	}
417
418	fn test_governance_input() -> OgmiosUtxo {
419		OgmiosUtxo { transaction: OgmiosTx { id: [16; 32] }, index: 0, ..Default::default() }
420	}
421
422	fn test_governance_data() -> GovernanceData {
423		GovernanceData { policy: test_governance_policy(), utxo: test_governance_input() }
424	}
425
426	fn version_oracle_data() -> PlutusScriptData {
427		scripts_data::version_oracle(UtxoId::new([255u8; 32], 0), NetworkIdKind::Testnet).unwrap()
428	}
429
430	fn version_oracle_validator_address() -> cardano_serialization_lib::Address {
431		version_oracle_data().validator.address(NetworkIdKind::Testnet)
432	}
433
434	fn version_oracle_policy_csl_script_hash() -> ScriptHash {
435		version_oracle_data().policy.csl_script_hash()
436	}
437
438	fn test_transaction_context() -> TransactionContext {
439		TransactionContext {
440			payment_key: payment_key(),
441			payment_key_utxos: vec![make_utxo(121u8, 3, 996272387, &payment_addr())],
442			network: NetworkIdKind::Testnet,
443			protocol_parameters: protocol_parameters(),
444			change_address: payment_addr(),
445		}
446	}
447
448	fn governance_script_cost() -> ExUnits {
449		ExUnits::new(&100u64.into(), &200u64.into())
450	}
451
452	fn versioning_script_cost() -> ExUnits {
453		ExUnits::new(&300u64.into(), &400u64.into())
454	}
455
456	fn test_costs() -> Costs {
457		Costs::new(
458			vec![
459				(test_governance_policy().script().script_hash().into(), governance_script_cost()),
460				(version_oracle_policy_csl_script_hash(), versioning_script_cost()),
461			]
462			.into_iter()
463			.collect(),
464			std::collections::HashMap::new(),
465		)
466	}
467}