Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permissioned pools #83

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion aiken.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ requirements = []
source = "github"

[etags]
"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1725207797, nanos_since_epoch = 651057000 }, "d79382d2b6ecb3aee9b0755c31d8a5bbafe88a7b3706d7fb8a52fd4d05818501"]
"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1732619791, nanos_since_epoch = 638622185 }, "33dce3a6dbfc58a92cc372c4e15d802f079f4958af941386d18980eb98439bb4"]
62 changes: 45 additions & 17 deletions lib/calculation/process.ak
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use aiken/cbor
use aiken/collection/dict.{Dict}
use aiken/collection/list
use aiken/crypto.{Blake2b_256, Hash}
use aiken/interval
use calculation/deposit
Expand Down Expand Up @@ -88,7 +89,10 @@ pub fn pool_input_to_state(
}

/// If the order is restricted to a specific pool, then make sure pool_ident matches that; otherwise just return true
fn validate_pool_id(order_pool_ident: Option<Ident>, pool_ident: Ident) -> Bool {
pub fn validate_pool_id(
order_pool_ident: Option<Ident>,
pool_ident: Ident,
) -> Bool {
when order_pool_ident is {
Some(i) -> i == pool_ident
None -> True
Expand Down Expand Up @@ -139,7 +143,7 @@ pub fn process_order(
// TODO: we can probably avoid returning the outputs, and just return a boolean
outputs: List<Output>,
// A continuation to call with the next pool state and the list of outputs; this is more efficient than constructing an object and tuples
continuation: fn(Int, Int, Int, List<Output>) -> Bool,
continuation: fn(Int, Int, Int, List<Output>, Bool) -> Bool,
) -> Bool {
// Returns the updated pool state, the correct list of outputs to resume from, and total fee charged by the order
when details is {
Expand Down Expand Up @@ -211,7 +215,13 @@ pub fn process_order(
min_received,
output,
)
continuation(new_pool_a, new_pool_b, pool_quantity_lp, rest_outputs)
continuation(
new_pool_a,
new_pool_b,
pool_quantity_lp,
rest_outputs,
False,
)
}
order.Deposit(assets) -> {
expect [output, ..rest_outputs] = outputs
Expand Down Expand Up @@ -241,7 +251,7 @@ pub fn process_order(
fee,
output,
)
continuation(new_a, new_b, new_lp, rest_outputs)
continuation(new_a, new_b, new_lp, rest_outputs, False)
}
order.Withdrawal(amount) -> {
expect [output, ..rest_outputs] = outputs
Expand Down Expand Up @@ -271,7 +281,7 @@ pub fn process_order(
fee,
output,
)
continuation(new_a, new_b, new_lp, rest_outputs)
continuation(new_a, new_b, new_lp, rest_outputs, True)
}
// NOTE: we decided not to implement zap, for time constraints, and because a zap can be easily implemented as a chained order, as it is in V1
// The cheaper fees the DAO voted on should make this acceptable
Expand Down Expand Up @@ -305,9 +315,9 @@ pub fn process_order(
// so we can skip over it
// TODO: can we just return used_output here instead of passing around the lists of outputs?
if used_output {
continuation(new_a, new_b, pool_quantity_lp, rest_outputs)
continuation(new_a, new_b, pool_quantity_lp, rest_outputs, False)
} else {
continuation(new_a, new_b, pool_quantity_lp, outputs)
continuation(new_a, new_b, pool_quantity_lp, outputs, False)
}
}
order.Record(policy) -> {
Expand All @@ -321,6 +331,7 @@ pub fn process_order(
pool_quantity_b,
pool_quantity_lp,
rest_outputs,
False,
)
}
}
Expand Down Expand Up @@ -370,8 +381,9 @@ pub fn process_orders(
// A recursive aggregator for the number of "simple" and "strategy" orders we see; used for computing the fee without traversing the list independently, since we're already walking this list
simple_count: Int,
strategy_count: Int,
withdrawal_only: Bool,
// A continuation to call with the final pool state; more efficient than constructing tuples / objects
continuation: fn(Int, Int, Int, Int, Int) -> Bool,
continuation: fn(Int, Int, Int, Int, Int, Bool) -> Bool,
) -> Bool {
// Returns the final pool state, and the count of each order type
// The main "pump" of the recursive loop is the input_order, which is a set of indices into the inputs list
Expand All @@ -385,6 +397,7 @@ pub fn process_orders(
pool_quantity_lp,
simple_count,
strategy_count,
withdrawal_only,
)
[(idx, sse, _), ..rest] -> {
// First, it's important to check that each order is processed only once;
Expand Down Expand Up @@ -436,6 +449,7 @@ pub fn process_orders(
new_b,
new_lp,
next_orders,
new_withdrawal_only,
<-
process_order(
pool_policy_a,
Expand Down Expand Up @@ -495,6 +509,7 @@ pub fn process_orders(
next_uniqueness_flag,
next_simple_count,
next_strategy_count,
new_withdrawal_only && withdrawal_only,
continuation,
)
}
Expand Down Expand Up @@ -575,19 +590,17 @@ test process_orders_test() {
}
let valid_range = interval.between(1, 2)

let input_order =
[(0, None, 0)]
let inputs =
[input]
let outputs =
[output]
let input_order = [(0, None, 0)]
let inputs = [input]
let outputs = [output]

let
new_a,
new_b,
new_lp,
simple,
strategies,
withdrawal_only,
<-
process_orders(
#"",
Expand Down Expand Up @@ -616,13 +629,15 @@ test process_orders_test() {
0,
0,
0,
True,
)

expect new_a == 1_001_000_000
expect new_b == 1_001_000_000
expect new_lp == 1_000_000_000
expect simple == 1
expect strategies == 0
expect withdrawal_only == False
Luivatra marked this conversation as resolved.
Show resolved Hide resolved
True
}

Expand Down Expand Up @@ -715,15 +730,15 @@ test process_30_shuffled_orders_test() {
(20, None, 0), (29, None, 0), (19, None, 0), (21, None, 0), (9, None, 0),
(25, None, 0), (6, None, 0), (4, None, 0), (3, None, 0), (15, None, 0),
]
let outputs =
[output]
let outputs = [output]

let
new_a,
new_b,
new_lp,
simple,
strategies,
withdrawal_only,
<-
process_orders(
#"",
Expand Down Expand Up @@ -752,7 +767,20 @@ test process_30_shuffled_orders_test() {
0,
0,
0,
True,
)

new_a == 1_030_000_000 && new_b == 1_030_000_000 && new_lp == 1_000_000_000 && simple == 30 && strategies == 0
new_a == 1_030_000_000 && new_b == 1_030_000_000 && new_lp == 1_000_000_000 && simple == 30 && strategies == 0 && !withdrawal_only
}

pub fn find_pool_output(outputs: List<Output>) -> (Output, PoolDatum) {
// Find the pool output; we can assume the pool output is the first output, because:
// - The ledger doesn't reorder outputs, just inputs
// - We check that the address is correct, so if the first output was to a different contract, we would fail
// - We check that the datum is the correct type, meaning we can't construct an invalid pool output
// - Later, we check that the pool output has the correct value, meaning it *must* contain the pool token, so we can't pay to the pool script multiple times
expect Some(pool_output) = list.head(outputs)
expect InlineDatum(output_datum) = pool_output.datum
expect output_datum: PoolDatum = output_datum
(pool_output, output_datum)
}
2 changes: 2 additions & 0 deletions lib/calculation/shared.ak
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,5 @@ pub fn small_pow2(exponent: Int) -> Int {
math.pow2(exponent)
}
}

pub const millis_per_day = 86_400_000
84 changes: 83 additions & 1 deletion lib/shared.ak
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use aiken/collection/list
use aiken/crypto.{Blake2b_256, Hash}
use aiken/primitive/bytearray
use cardano/address.{Credential, Script}
use cardano/assets.{AssetName, PolicyId}
use cardano/assets.{AssetName, PolicyId, Value, ada_policy_id}
use cardano/transaction.{
DatumHash, InlineDatum, Input, NoDatum, Output, OutputReference, Transaction,
find_input,
Expand Down Expand Up @@ -51,6 +51,88 @@ pub fn datum_of(
}
}

/// Check that the UTXO contents are correct given a specific pool outcome
/// In particular, it must have the final A reserves, the final B reserves, the pool NFT, and the protocol fees
pub fn has_expected_pool_value(
pool_script_hash: PolicyId,
identifier: Ident,
output_value: Value,
pool_policy_a: PolicyId,
pool_asset_name_a: AssetName,
pool_quantity_a: Int,
pool_policy_b: PolicyId,
pool_asset_name_b: AssetName,
pool_quantity_b: Int,
final_lp: Int,
final_protocol_fees: Int,
) -> Bool {
// Asset A *could* be ADA; in which case there should be 3 tokens on the output
// (ADA, Asset B, and the NFT)
if pool_policy_a == ada_policy_id {
let actual =
list.foldl(
assets.flatten(output_value),
// (token count, lovelace amount, token b amount, pool nft amount)
(0, 0, 0, 0),
fn(asset, acc) {
let token_count = acc.1st + 1
if asset.1st == pool_policy_a {
(token_count, acc.2nd + asset.3rd, acc.3rd, acc.4th)
} else if asset.1st == pool_policy_b && asset.2nd == pool_asset_name_b {
(token_count, acc.2nd, acc.3rd + asset.3rd, acc.4th)
} else {
expect asset == (pool_script_hash, pool_nft_name(identifier), 1)
(token_count, acc.2nd, acc.3rd, acc.4th + 1)
}
},
)
// If we're withdrawing the last bit of liquidity, we just have ADA and the pool token
let expected =
if final_lp == 0 {
expect pool_quantity_a == 0
expect pool_quantity_b == 0
(2, final_protocol_fees, 0, 1)
} else {
(3, final_protocol_fees + pool_quantity_a, pool_quantity_b, 1)
}
// Rather than constructing a value directly (which can be expensive)
// we can just compare the expected token count and amounts with a single pass over the value
expected == actual
} else {
// Asset A isn't ADA, Asset B will *never* be ADA; in this case, there should be 4 tokens on the output:
// ADA, the Pool NFT, Asset A, and Asset B
let actual =
list.foldl(
assets.flatten(output_value),
// (token count, lovelace amount, token a amount, token b amount, pool nft amount)
(0, 0, 0, 0, 0),
fn(asset, acc) {
let token_count = acc.1st + 1
if asset.1st == ada_policy_id {
(token_count, acc.2nd + asset.3rd, acc.3rd, acc.4th, acc.5th)
} else if asset.1st == pool_policy_a && asset.2nd == pool_asset_name_a {
(token_count, acc.2nd, acc.3rd + asset.3rd, acc.4th, acc.5th)
} else if asset.1st == pool_policy_b && asset.2nd == pool_asset_name_b {
(token_count, acc.2nd, acc.3rd, acc.4th + asset.3rd, acc.5th)
} else {
expect asset == (pool_script_hash, pool_nft_name(identifier), 1)
(token_count, acc.2nd, acc.3rd, acc.4th, acc.5th + 1)
}
},
)
// If we're withdrawing the last bit of liquidity, we just have ADA and the pool token
let expected =
if final_lp == 0 {
expect pool_quantity_a == 0
expect pool_quantity_b == 0
(2, final_protocol_fees, 0, 0, 1)
} else {
(4, final_protocol_fees, pool_quantity_a, pool_quantity_b, 1)
}
expected == actual
}
}

/// Find the **input** (which was the output of some other transaction) for which we're actually evaluating the script to determine if it is spendable
/// Also called "own_input" in places
pub fn spent_output(
Expand Down
2 changes: 2 additions & 0 deletions lib/tests/examples/ex_pool.ak
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ fn mk_pool_datum() -> PoolDatum {
fee_manager: None,
market_open: 100,
protocol_fees: 10000000,
condition: None,
condition_datum: None,
}
}

Expand Down
31 changes: 31 additions & 0 deletions lib/types/conditions/permissioned.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use aiken/crypto.{
Blake2b_256, Hash, Signature, VerificationKey, VerificationKeyHash,
}
use cardano/transaction.{ValidityRange}
use shared.{Ident}
use types/order.{Destination}

pub type ComplianceToken {
token: TokenData,
//A signature from the compliance oracle for serialized TokenData
oracle_signature: Signature,
}

pub type TokenData {
// The users DID Identifier
did: Ident,
//The users public key
user_key: VerificationKeyHash,
//The destination address
destination: Destination,
//Blake2b-256 hash of the cbor serialized details from the order
order_hash: Hash<Blake2b_256, Data>,
//A valid range
validity_range: ValidityRange,
//The public key of the oracle
oracle_key: VerificationKey,
}

pub type PermissionedDatum {
whitelisted_oracles: List<VerificationKeyHash>,
}
4 changes: 4 additions & 0 deletions lib/types/conditions/trading_hours.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub type TradingHoursDatum {
open_time: Int,
close_time: Int,
}
3 changes: 3 additions & 0 deletions lib/types/pool.ak
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use aiken/crypto.{ScriptHash}
use shared.{AssetClass, Ident}
use sundae/multisig
use types/order.{SignedStrategyExecution}
Expand Down Expand Up @@ -31,6 +32,8 @@ pub type PoolDatum {
/// and withdrawals never have to be for the full amount.
/// TODO: should we add a field to the settings object to set a minimum initial protocol_fees on pool mint?
protocol_fees: Int,
condition: Option<ScriptHash>,
condition_datum: Option<Data>,
}

/// A pool UTXO can be spent for two purposes:
Expand Down
Loading