diff --git a/lib/staking_contracts/datums.ak b/lib/staking_contracts/datums.ak
index 090f679..1708cce 100644
--- a/lib/staking_contracts/datums.ak
+++ b/lib/staking_contracts/datums.ak
@@ -57,6 +57,7 @@ pub type StakePoolDatum {
pub type StakePoolRedeemer {
//Index of the reward setting to be used ina locking tx.
+ // Spec 1.5.8
reward_index: Int,
}
@@ -88,16 +89,19 @@ pub type StakeProxyDatum {
nft_policy_id: PolicyId,
}
+// Spec 2.5.13
pub fn to_stake_pool_datum(data: Data) -> StakePoolDatum {
expect my_datum: StakePoolDatum = data
my_datum
}
+// UNUSED
pub fn to_stake_proxy_datum(data: Data) -> StakeProxyDatum {
expect my_datum: StakeProxyDatum = data
my_datum
}
+// Spec 2.5.14, 2.6.8.6, 4.3.2.5
pub fn to_time_lock_datum(data: Data) -> TimeLockDatum {
expect my_datum: TimeLockDatum = data
my_datum
diff --git a/lib/staking_contracts/stake_nft_mint.ak b/lib/staking_contracts/stake_nft_mint.ak
index d7dfc5c..e4a47f2 100644
--- a/lib/staking_contracts/stake_nft_mint.ak
+++ b/lib/staking_contracts/stake_nft_mint.ak
@@ -22,100 +22,155 @@ use staking_contracts/datums.{
use staking_contracts/utils.{hour_millis, must_start_after, time_to_date_string}
use sundae/multisig.{Signature}
+// Spec 2.4
pub type StakeNFTMintRedeemer {
+ // Spec 2.4.1
stake_pool_index: Int,
+ // Spec 2.4.2
time_lock_index: Int,
+ // Spec 2.4.3
mint: Bool,
}
+// Spec 2.5
pub fn mint_transaction(
redeemer: StakeNFTMintRedeemer,
ctx: ScriptContext,
time_lock_hash: ByteArray,
) -> Bool {
when ctx.purpose is {
+ // Spec 2.5.2
Mint(own_policy) -> {
//Retrieve all minted assets into an array
let minted = from_minted_value(ctx.transaction.mint) |> flatten()
//Only 2 assets minted
+ // Spec 2.5.3
expect 2 == list.length(minted)
+
//Look for the reference nft (CIP-68)
+ // Spec 2.5.4 - "exactly one token"
expect [reference_nft] =
+ // Spec 2.5.4 - "being minted"
minted
+ // Spec 2.5.4 - "with"
|> filter(
fn(mt) {
- mt.1st == own_policy && take(mt.2nd, 4) == reference_prefix
+ // Spec 2.5.4 - "a policy ID of Stake NFT"
+ mt.1st == own_policy &&
+ // Spec 2.5.4 - "and an asset name prefixed by"
+ take(mt.2nd, 4) == reference_prefix
},
)
//Look for the stake nft (CIP-68)
+ // Spec 2.5.5 - "exactly one token"
expect [stake_nft] =
+ // Spec 2.5.5 - "being minted"
minted
+ // Spec 2.5.5 - "with"
|> filter(
fn(mt) {
- mt.1st == own_policy && take(mt.2nd, 4) == stake_nft_prefix
+ // Spec 2.5.5 - "a policy ID of Stake NFT"
+ mt.1st == own_policy &&
+ // Spec 2.5.5 - "and an asset name prefixed by"
+ take(mt.2nd, 4) == stake_nft_prefix
},
)
//Asset name without CIP-68 prefix
+ // Spec 2.5.6
let asset_name = drop(reference_nft.2nd, 4)
//Ensure the stake nft has the same asset name as the reference nft
+ // Spec 2.5.7 - n.b. included in the `and` condition below
let correct_stake_nft_name = drop(stake_nft.2nd, 4) == asset_name
//only one reference nft minted
+ // Spec 2.5.8
expect 1 == reference_nft.3rd
//only one stake nft minted
+ // Spec 2.5.8
expect 1 == stake_nft.3rd
+
//The stake pool input
+ // Spec 2.5.9
expect Some(stake_pool_input) =
ctx.transaction.inputs |> at(redeemer.stake_pool_index)
+
+ // Spec 2.5.10 - n.b. stake_pool_cred checked below
expect ScriptCredential(stake_pool_cred) =
stake_pool_input.output.address.payment_credential
+
//The time lock output
+ // Spec 2.5.11
expect Some(time_lock_output) =
ctx.transaction.outputs |> at(redeemer.time_lock_index)
+
+ // Spec 2.5.12 - n.b. time_lock_cred checked below
expect ScriptCredential(time_lock_cred) =
time_lock_output.address.payment_credential
+
//Fetch inlinedatum from stakepool input and cast to StakePoolDatum
+ // Spec 2.5.13
expect InlineDatum(stake_pool_input_datum) = stake_pool_input.output.datum
let stake_pool_datum = to_stake_pool_datum(stake_pool_input_datum)
//Fetch inlinedatum from timelock output and cast to TimeLockDatum
+ // Spec 2.5.14
expect InlineDatum(time_lock_output_datum) = time_lock_output.datum
let time_lock_datum = to_time_lock_datum(time_lock_output_datum)
//The amount of tokens to be locked (no decimals taken into account)
+ // Spec 2.5.15
let raw_amount =
time_lock_output.value
|> quantity_of(stake_pool_datum.policy_id, stake_pool_datum.asset_name)
+ // Spec 2.5.16 - n.b. actually checked below in the `and` condition
let reference_nft_in_time_lock =
1 == quantity_of(time_lock_output.value, own_policy, reference_nft.2nd)
//Construct the proper asset name using the unique components
+ // Spec 2.5.17 - "let proper_asset_name" be
let proper_asset_name =
- blake2b_256(serialise(stake_pool_input.output_reference))
- |> take(28)
+ // Spec 2.5.17 - "the blake2b_256 hash of"
+ blake2b_256(
+ // Spec 2.5.17 - "the serialized"
+ serialise(
+ // Spec 2.5.17 - "stake_pool_input.output_reference"
+ stake_pool_input.output_reference
+ )
+ )
+ // Spec 2.5.17 - The first 28 bytes of
+ |> take(28)
//Construct the proper metadata nft name
+ // Spec 2.5.18 - "let proper_meta_name be"
let proper_meta_name =
list.foldl(
[
+ // Spec 2.5.18 - Stake NFT
"Stake NFT ",
+ // Spec 2.5.18 - "the reward token asset name"
stake_pool_datum.asset_name,
+ // Spec 2.5.18 - a dash
" - ",
+ // Spec 2.5.18 - and some human readable expression of lock_until
+ // n.b. the details of this method are not audited, but the impact is low
time_to_date_string(time_lock_datum.extra.lock_until),
],
"",
+ // Spec 2.5.18 - "The concatenation of"
fn(el, sum) { bytearray.concat(sum, el) },
)
//Validations
//Make sure the asset name is as expected
+ // Spec 2.5.19 - n.b. included in the `and` condition below
let correct_reference_name = asset_name == proper_asset_name
+ // Spec 2.5.20 - n.b. included in the `and` condition below
let correct_stake_nft_set =
time_lock_datum.extra.time_lock_nft == bytearray.concat(
own_policy,
@@ -123,43 +178,65 @@ pub fn mint_transaction(
)
//Ensure the metadata token name is as expected
+ // Spec 2.5.21 - n.b. included in the `and` condition below
let correct_meta_name =
dict.get(time_lock_datum.metadata, "name") == Some(proper_meta_name)
//Make sure an extra field is present in the metadata showing the tokens locked by the nft
+ // Spec 2.5.22
let proper_meta_amount =
list.foldl(
[
+ // Spec 2.5.22 - `[(`
"[",
"(",
+ // Spec 2.5.22 - The reward token policy ID
bytearray.from_string(bytearray.to_hex(stake_pool_datum.policy_id)),
+ // Spec 2.5.22 - `,`
",",
+ // Spec 2.5.22 - The reward token asset name
bytearray.from_string(bytearray.to_hex(stake_pool_datum.asset_name)),
+ // Spec 2.5.22 - `,`
",",
+ // Spec 2.5.22 - `raw_amount`
bytearray.from_string(string.from_int(raw_amount)),
+ // Spec 2.5.22 - `)]`
")",
"]",
],
"",
+ // Spec 2.5.22 "the concatenation of"
fn(el, sum) { bytearray.concat(sum, el) },
)
+ // Spec 2.5.22 - n.b. included in the `and` condition below
let correct_meta_amount =
+ // Spec 2.5.22 - The CIP-68 metadata at "locked_amount" must be
dict.get(time_lock_datum.metadata, "locked_assets") == Some(
proper_meta_amount,
)
//Verify script hashes
+ // Spec 2.5.10 - n.b. stake_pool_cred comes from stake_pool_input, and this is included in the `and` below
let correct_stake_pool = own_policy == stake_pool_cred
+ // Spec 2.5.12 - n.b. time_lock_cred comes from time_lock_output, and this is included in the `and` below
let correct_time_lock = time_lock_hash == time_lock_cred
and {
+ // Spec 2.5.16
reference_nft_in_time_lock?,
+ // Spec 2.5.20
correct_stake_nft_set?,
+ // Spec 2.5.7
correct_stake_nft_name?,
+ // Spec 2.5.19
correct_reference_name?,
+ // Spec 2.5.21
correct_meta_name?,
+ // Spec 2.5.22
correct_meta_amount?,
+ // Spec 2.5.10
correct_stake_pool?,
+ // Spec 2.5.12
correct_time_lock?,
}
}
@@ -167,12 +244,16 @@ pub fn mint_transaction(
}
}
+// Spec 2.6
pub fn burn_transaction(ctx: ScriptContext, time_lock_hash: ByteArray) -> Bool {
when ctx.purpose is {
+ // Spec 2.6.2
Mint(own_policy) -> {
//Retrieve all minted assets into an array
+ // Spec 2.6.3
let burned = from_minted_value(ctx.transaction.mint) |> flatten()
//Look for the reference nfts (CIP-68)
+ // Spec 2.6.5
let reference_nfts =
burned
|> filter(
@@ -180,8 +261,10 @@ pub fn burn_transaction(ctx: ScriptContext, time_lock_hash: ByteArray) -> Bool {
mt.1st == own_policy && take(mt.2nd, 4) == reference_prefix
},
)
+ // Spec 2.6.4
let burned_count = list.length(burned)
//Look for the stake nft (CIP-68)
+ // Spec 2.6.6
let stake_nfts =
burned
|> filter(
@@ -189,34 +272,50 @@ pub fn burn_transaction(ctx: ScriptContext, time_lock_hash: ByteArray) -> Bool {
mt.1st == own_policy && take(mt.2nd, 4) == stake_nft_prefix
},
)
+
+ // Spec 2.6.7.1
expect list.length(reference_nfts) == list.length(stake_nfts)
+ // Spec 2.6.7.2
let nft_count = list.length(reference_nfts) + list.length(stake_nfts)
//Ensure that only CIP-68 nft's are burned
+ // Spec 2.6.7.2 - n.b. included in the `and` condition below
let correct_number_burned = burned_count == nft_count
+ // Spec 2.6.8
let correct_burns =
+ // Spec 2.6.8 - "For each asset"
list.all(
+ // Spec 2.6.8 - "in reference_nfts"
reference_nfts,
fn(reference_nft) {
//Asset name without CIP-68 prefix
+ // Spec 2.6.8.1
let asset_name = drop(reference_nft.2nd, 4)
+ // Spec 2.6.8.2
expect Some(stake_nft) =
stake_nfts |> find(fn(sk) { drop(sk.2nd, 4) == asset_name })
when reference_nft.3rd is {
//Burn scenario (unlock of locked tokens)
+ // Spec 2.6.8.3 - n.b. any other case is immediately False
-1 -> {
//Full asset unit
+ // Used in Spec 2.6.8.7
let burned_unit = bytearray.concat(stake_nft.1st, stake_nft.2nd)
//The time lock input that contains the reference nft
+ // Spec 2.6.8.5 - "There must be", "Let this input be the time_lock_input"
expect Some(time_lock_input) =
+ // Spec 2.6.8.5 - "input"
ctx.transaction.inputs
+ // Spec 2.6.8.5 - "at least one"
|> find(
fn(i) {
+ // Spec 2.6.8.5 - "with the time_lock_hash script credential"
i.output.address.payment_credential == ScriptCredential(
time_lock_hash,
+ // Spec 2.6.8.5 - "with the reference NFT in the value"
) && quantity_of(
i.output.value,
reference_nft.1st,
@@ -226,31 +325,40 @@ pub fn burn_transaction(ctx: ScriptContext, time_lock_hash: ByteArray) -> Bool {
)
//The inline datum belonging to the time lock inptu cast to TimeLockDatum
+ // Spec 2.6.8.6
expect InlineDatum(time_lock_input_datum) =
time_lock_input.output.datum
let time_lock_datum = to_time_lock_datum(time_lock_input_datum)
//Ensure the tokens burned match the timelock nft
+ // Spec 2.6.8.7 - n.b. included in the `and` condition below
let correct_token_burned =
time_lock_datum.extra.time_lock_nft == burned_unit
//Make sure both tokens are burned
+ // Spec 2.6.8.4 - n.b. equivalent to checking that stake_nft.3rd is equal to -1
let both_burned = stake_nft.3rd == reference_nft.3rd
and {
+ // Spec 2.6.8.8
must_start_after(
ctx.transaction.validity_range,
time_lock_datum.extra.lock_until,
)?,
+ // Spec 2.6.8.7
correct_token_burned?,
+ // 2.6.8.4
both_burned?,
}
}
+ // Spec 2.6.8.3
_ -> False
}
},
)
and {
+ // Spec 2.6.7
correct_number_burned?,
+ // Spec 2.6.8
correct_burns?,
}
}
diff --git a/lib/staking_contracts/stake_pool.ak b/lib/staking_contracts/stake_pool.ak
index f0fdef7..a6d3a82 100644
--- a/lib/staking_contracts/stake_pool.ak
+++ b/lib/staking_contracts/stake_pool.ak
@@ -21,10 +21,13 @@ use staking_contracts/utils.{
}
use sundae/multisig.{Signature, satisfied}
+// Spec 1.4
pub fn update_transaction(datum: StakePoolDatum, ctx: ScriptContext) -> Bool {
when ctx.purpose is {
//Ensure the utxo is spent by the owner
+ // Spec 1.4.1
Spend(_) ->
+ // Spec 1.4.2
satisfied(
datum.owner,
ctx.transaction.extra_signatories,
@@ -35,6 +38,7 @@ pub fn update_transaction(datum: StakePoolDatum, ctx: ScriptContext) -> Bool {
}
}
+// Spec 1.5
pub fn lock_transaction(
datum: StakePoolDatum,
redeemer: StakePoolRedeemer,
@@ -43,6 +47,7 @@ pub fn lock_transaction(
) -> Bool {
when ctx.purpose is {
//Ensure the utxo is spent in a valid locking tx
+ // Spec 1.5.1
Spend(my_output_reference) ->
is_proper_locking_tx(
my_output_reference,
@@ -55,6 +60,7 @@ pub fn lock_transaction(
}
}
+// Spec 1.5
fn is_proper_locking_tx(
my_output_reference: OutputReference,
datum: StakePoolDatum,
@@ -62,10 +68,12 @@ fn is_proper_locking_tx(
ctx: ScriptContext,
time_lock_hash: ByteArray,
) {
+ // Spec 1.5.2
expect Finite(lower_bound) =
ctx.transaction.validity_range.lower_bound.bound_type
expect lower_bound >= datum.open_time
//Find the input guarded by this validator
+ // 1.5.3 "which is known by finding the transaction input with the same output reference from the script context purpose"
expect Some(self) =
ctx.transaction.inputs
|> transaction.find_input(my_output_reference)
@@ -73,6 +81,7 @@ fn is_proper_locking_tx(
let self_input = self.output
//Find amount of inputs with stake pool script, should be exactly 1
+ // Spec 1.5.3 - n.b. checked in the `and` below
let exactly_one_stake_pool_input =
length(
filter_inputs_by_credential(
@@ -82,52 +91,65 @@ fn is_proper_locking_tx(
) == 1
//We need to ensure the output of the tx has the same validator attached
+ // Spec 1.5.4 - n.b. existance, but not uniqueness (there is at least one)
expect ScriptCredential(self_script) = self_input.address.payment_credential
let self_outputs =
ctx.transaction.outputs
|> transaction.find_script_outputs(self_script)
expect Some(self_output) = self_outputs |> at(0)
+ // Spec 1.5.5 - n.b. in particular, The staking address is unchanged
expect self_input.address == self_output.address
+
//The output needs to include a utxo with time lock validator
+ // Spec 1.5.6 - n.b. existance, but not uniqueness (there is at least one)
let time_lock_outputs =
ctx.transaction.outputs
|> transaction.find_script_outputs(time_lock_hash)
expect Some(time_lock) = time_lock_outputs |> at(0)
+ // Spec 1.5.7 - inline datum of time TimeLockDatum
expect InlineDatum(time_lock_data) = time_lock.datum
-
let time_lock_datum = to_time_lock_datum(time_lock_data)
//We use the reward setting as indicated by the submitter of the transaction
+ // Spec 1.5.9 - n.b. manipulation of reward_index will fail, because either time or reward quantity will be incorrect
expect Some(reward_setting) =
datum.reward_settings |> at(redeemer.reward_index)
+
//Find out current stake_pool token quantity
+ // Spec 1.5.13.1 "be the quantity of reward token in the stake pool input"
let stake_pool_size =
self_input.value
|> value.quantity_of(datum.policy_id, datum.asset_name)
//Find stake_pool token quantity in output
+ // Spec 1.5.13.1 "the quantity of the same in the stake pool output"
let new_stake_pool_size =
self_output.value
|> value.quantity_of(datum.policy_id, datum.asset_name)
//Difference between old and new quantity is equal to the reward to the staker
+ // Spec 1.5.13.1 - n.b. withdrawn_amount => reward
let reward = stake_pool_size - new_stake_pool_size
//The locked amount in the time lock output
+ // Spec 1.5.13.2 - n.b. total_locked_output => locked_amount
let locked_amount =
time_lock.value
|> value.quantity_of(datum.policy_id, datum.asset_name)
//The staked amount is deduced from the locked amount and the reward
+ // Spec 1.5.13.3 - n.b. derived_user_locked_output => staked_amount
let staked_amount = locked_amount - reward
//Ensure the locked amount matches the reward setting
+ // Spec 1.5.13.4 + 1.5.13.5 - n.b. checked in the `and` below
let correct_locked_amount =
locked_amount == add_reward(staked_amount, reward_setting.reward_multiplier)
//Make sure no other assets have been altered in the stake pool output compared to the input
+ // Spec 1.5.14.1 - n.b. checked in the `and` below
let correct_output_value =
value.without_lovelace(self_input.value) == add(
value.without_lovelace(self_output.value),
@@ -137,11 +159,16 @@ fn is_proper_locking_tx(
)
//Allow higher lovelace amount to support increase of minutxo
+ // Spec 1.5.14.2
expect
value.lovelace_of(self_input.value) <= value.lovelace_of(self_output.value)
+
//Make sure settings have not been altered
+ // Spec 1.5.7 - "with the same value as the datum attached to the input"
+ // n.b. checked in the `and` below
let same_datum = InlineDatum(datum) == self_output.datum
-
+
+ // Spec 1.5.15 - n.b. included in `and` below
let one_stake_nft_minted =
1 == value.quantity_of(
value.from_minted_value(ctx.transaction.mint),
@@ -149,16 +176,25 @@ fn is_proper_locking_tx(
bytearray.drop(time_lock_datum.extra.time_lock_nft, 28),
)
+ // Spec 1.5.16
expect bytearray.take(time_lock_datum.extra.time_lock_nft, 28) == self_script
+
and {
+ // Spec 1.5.15
one_stake_nft_minted?,
+ // Spec 1.5.3
exactly_one_stake_pool_input?,
//Ensure exactly one stake pool output
+ // Spec 1.5.4 - n.b. uniqueness
must_have_exactly_one_output(self_outputs)?,
//Ensure exactly one time lock output
+ // Spec 1.5.6 - n.b. uniqueness
must_have_exactly_one_output(time_lock_outputs)?,
+ // Spec 1.5.7
same_datum?,
+ // Spec 1.5.13.4 + 1.5.13.5
correct_locked_amount?,
+ // Spec 1.5.11 + 1.5.12
//Ensure that the time lock does not expire too soon
must_be_locked_until(
ctx.transaction.validity_range,
@@ -166,6 +202,7 @@ fn is_proper_locking_tx(
reward_setting.ms_locked,
hour_millis,
)?,
+ // Spec 1.5.14.1
correct_output_value?,
}
}
diff --git a/lib/staking_contracts/utils.ak b/lib/staking_contracts/utils.ak
index d0a3e99..758d33a 100644
--- a/lib/staking_contracts/utils.ak
+++ b/lib/staking_contracts/utils.ak
@@ -15,69 +15,100 @@ use staking_contracts/datums.{
}
use sundae/multisig.{Signature}
+// Spec 1.5.3, 4.3.3
pub fn filter_inputs_by_credential(inputs: List, cred: Credential) {
inputs |> filter(fn(i) { i.output.address.payment_credential == cred })
}
+// Spec 1.5.3 + 1.5.6 - uniqueness of outputs
pub fn must_have_exactly_one_output(outputs: List