Skip to content

Commit

Permalink
Merge pull request #50 from SundaeSwap-finance/pi/oracles
Browse files Browse the repository at this point in the history
Add "Oracles"
  • Loading branch information
Quantumplation authored Mar 31, 2024
2 parents e3b7ca3 + be2131c commit 7181c12
Show file tree
Hide file tree
Showing 8 changed files with 572 additions and 2 deletions.
9 changes: 9 additions & 0 deletions lib/calculation/process.ak
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use aiken/transaction/value.{Value, PolicyId}
use aiken/time.{PosixTime}
use calculation/deposit
use calculation/donation
use calculation/record
use calculation/shared.{
PoolState, check_and_set_unique, unsafe_fast_index_skip_with_tail,
} as calc_shared
Expand Down Expand Up @@ -247,6 +248,14 @@ pub fn process_order(
(next, outputs)
}
}
order.Record(policy) -> {
// Make sure the scooper can only take up to the max fee the user has agreed to
expect [output, ..rest_outputs] = outputs
let fee = amortized_base_fee + simple_fee
expect max_protocol_fee >= fee
expect record.check_record(input, destination, fee, output, policy)
(initial, rest_outputs)
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions lib/calculation/record.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use aiken/transaction.{Output}
use aiken/transaction/value.{ada_policy_id, ada_asset_name, PolicyId, AssetName}
use types/order.{Destination, Fixed, Self}

pub fn check_record(
input: Output,
destination: Destination,
actual_protocol_fee: Int,
output: Output,
asset_id: (PolicyId, AssetName),
) -> Bool {
// Make sure all of the funds from the input make it into the oracle output
// Note that the accuracy of the oracle is handled by the mint policy, since it has easier
// access to the final state of the order

// Theoretically, this could be satisfied if someone specified an asset_id / policy_id that
// came from another input, rather than one that had to be minted here; but that would be on them
// and maybe there's a good reason for someone to want that behavior.
// We know that the `oracle` token we provide is secure against this, because it must be minted into
// its own spending policy, and it must be burned when spent.
let remainder = input.value
|> value.add(ada_policy_id, ada_asset_name, -actual_protocol_fee)
|> value.add(asset_id.1st, asset_id.2nd, 1)

and {
output.value == remainder,
when destination is {
// The datum of the destination can be used by the oracle minting policy
// but we don't check it here
// E.g. for a standard sundae oracle, the datum encodes the owner, and
// then the oracle minting policy checks that the scooper minted an oracle
// with the correct owner
Fixed(address, _) -> output.address == address
// It doesn't make sense for an oracle to chain into an oracle
Self -> False
}
}
}
4 changes: 4 additions & 0 deletions lib/shared.ak
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,7 @@ test count_orders_test() {

count_orders(inputs) == 10
}

pub fn oracle_sft_name() {
"oracle"
}
165 changes: 165 additions & 0 deletions lib/tests/aiken/record.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use aiken/interval
use aiken/transaction.{NoDatum, InlineDatum, Output}
use aiken/transaction/credential.{Address, VerificationKeyCredential, ScriptCredential}
use aiken/transaction/value
use calculation/record.{check_record}
use shared
use sundae/multisig
use types/oracle.{OracleDatum} as types_oracle
use types/order.{Fixed}

test record() {
let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000"
let oracle_name = shared.oracle_sft_name()
let addr =
Address(
ScriptCredential(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
None,
)
let input_value =
value.from_lovelace(4_500_000)
let destination = Fixed {
address: addr,
datum: NoDatum,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(Void),
reference_script: None,
}
let output = Output {
address: addr,
value: value.from_lovelace(2_000_000)
|> value.add(oracle_policy_id, oracle_name, 1),
datum: InlineDatum(OracleDatum {
owner: multisig.AnyOf([]),
valid_range: interval.between(0, 1),
pool_ident: #"00",
reserve_a: (#"00", #"00", 1_000_000),
reserve_b: (#"00", #"00", 1_000_000),
circulating_lp: (#"00", #"00", 1_000_000),
}),
reference_script: None,
}
check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name))
}

test record_must_pay_to_destination() fail {
let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000"
let oracle_name = shared.oracle_sft_name()
let addr =
Address(
ScriptCredential(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
None,
)
let input_value =
value.from_lovelace(4_500_000)
let destination = Fixed {
address: addr,
datum: NoDatum,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(Void),
reference_script: None,
}
let output = Output {
address: Address(VerificationKeyCredential(#"00"), None),
value: value.from_lovelace(2_000_000)
|> value.add(oracle_policy_id, oracle_name, 1),
datum: InlineDatum(OracleDatum {
owner: multisig.AnyOf([]),
valid_range: interval.between(0, 1),
pool_ident: #"00",
reserve_a: (#"00", #"00", 1_000_000),
reserve_b: (#"00", #"00", 1_000_000),
circulating_lp: (#"00", #"00", 1_000_000),
}),
reference_script: None,
}
check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name))
}

test record_must_have_token() fail {
let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000"
let oracle_name = shared.oracle_sft_name()
let addr =
Address(
ScriptCredential(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
None,
)
let input_value =
value.from_lovelace(4_500_000)
let destination = Fixed {
address: addr,
datum: NoDatum,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(Void),
reference_script: None,
}
let output = Output {
address: addr,
value: value.from_lovelace(2_000_000),
datum: InlineDatum(OracleDatum {
owner: multisig.AnyOf([]),
valid_range: interval.between(0, 1),
pool_ident: #"00",
reserve_a: (#"00", #"00", 1_000_000),
reserve_b: (#"00", #"00", 1_000_000),
circulating_lp: (#"00", #"00", 1_000_000),
}),
reference_script: None,
}
check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name))
}

test record_bad_datum() fail {
let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000"
let oracle_name = shared.oracle_sft_name()
let addr =
Address(
ScriptCredential(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
None,
)
let input_value =
value.from_lovelace(4_500_000)
|> value.add(oracle_policy_id, oracle_name, 1)
let destination = Fixed {
address: addr,
datum: NoDatum,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(Void),
reference_script: None,
}
let output = Output {
address: addr,
value: value.from_lovelace(2_000_000)
|> value.add(oracle_policy_id, oracle_name, 1),
datum: InlineDatum(OracleDatum {
owner: multisig.AnyOf([]),
valid_range: interval.between(0, 1),
pool_ident: #"00",
reserve_a: (#"00", #"00", 1_000_000),
reserve_b: (#"00", #"00", 1_000_000),
circulating_lp: (#"00", #"00", 1_000_000),
}),
reference_script: None,
}
check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name))
}
21 changes: 21 additions & 0 deletions lib/types/oracle.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use sundae/multisig
use aiken/transaction.{ValidityRange}
use shared.{SingletonValue, Ident}

pub type OracleDatum {
// The owner who is allowed to reclaim this datum at the end
owner: multisig.MultisigScript,
// The valid range for the scoop transaction that produced this Datum, which gives a confidence interval for when this price was valid
valid_range: ValidityRange,
// The pool identifier that this datum was produced for
pool_ident: Ident,
// The reserve *after* the scoop in question
reserve_a: SingletonValue,
reserve_b: SingletonValue,
circulating_lp: SingletonValue,
}

pub type OracleRedeemer {
Mint(Ident, List<Int>)
Burn
}
6 changes: 5 additions & 1 deletion lib/types/order.ak
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use aiken/transaction.{Datum, OutputReference, ValidityRange}
use aiken/transaction/credential.{Address, VerificationKey, Signature, Script}
use aiken/transaction/value.{PolicyId, AssetName}
use shared.{Ident, SingletonValue}
use sundae/multisig.{MultisigScript}
use aiken/hash.{Hash, Blake2b_224}
Expand Down Expand Up @@ -75,6 +76,9 @@ pub type Order {
/// Donate some value to the pool; Likely not useful for an end user, but lets other
/// protocols create interesting options: for example, an incentive program for their liquidity providers, etc.
Donation { assets: (SingletonValue, SingletonValue) }
// Record, like the verb; let some script witness a scoop, and enforce some property about the output
// Used, for example, to record the state of the pool for an on-chain oracle, etc.
Record { policy: (PolicyId, AssetName) }
}

/// An order can be spent either to Scoop (execute) it, or to cancel it
Expand Down Expand Up @@ -108,4 +112,4 @@ pub type SignedStrategyExecution {
strategy: StrategyExecution,
/// An ed25519 signature of the serialized `strategy`
signature: Option<Signature>,
}
}
Loading

0 comments on commit 7181c12

Please sign in to comment.