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