├── .gitignore ├── aiken.lock ├── aiken.toml ├── .github └── workflows │ └── tests.yml ├── lib └── course │ ├── types.ak │ └── tests.ak ├── validators ├── b │ ├── oneshot.ak │ ├── oracle.ak │ └── oracle-test.ak ├── always │ └── always.ak ├── vulnerabilities │ ├── vuln-tests.ak │ ├── dust-attack.ak │ ├── stake-attack.ak │ ├── double-satisfaction.ak │ └── infinite-mint.ak └── a │ ├── market.ak │ └── market-test.ak ├── README.md └── plutus.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Aiken compilation artifacts 2 | artifacts/ 3 | # Aiken's project working directory 4 | build/ 5 | # Aiken's default documentation export 6 | docs/ 7 | -------------------------------------------------------------------------------- /aiken.lock: -------------------------------------------------------------------------------- 1 | # This file was generated by Aiken 2 | # You typically do not need to edit this file 3 | 4 | [[requirements]] 5 | name = "aiken-lang/stdlib" 6 | version = "v2.1.0" 7 | source = "github" 8 | 9 | [[packages]] 10 | name = "aiken-lang/stdlib" 11 | version = "v2.1.0" 12 | requirements = [] 13 | source = "github" 14 | 15 | [etags] 16 | -------------------------------------------------------------------------------- /aiken.toml: -------------------------------------------------------------------------------- 1 | name = "rhystmorgan/course" 2 | version = "0.0.0" 3 | compiler = "v1.1.3" 4 | plutus = "v3" 5 | license = "Apache-2.0" 6 | description = "Aiken contracts for project 'rhystmorgan/course'" 7 | 8 | [repository] 9 | user = "rhystmorgan" 10 | project = "course" 11 | platform = "github" 12 | 13 | [[dependencies]] 14 | name = "aiken-lang/stdlib" 15 | version = "v2.1.0" 16 | source = "github" 17 | 18 | [config] 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: aiken-lang/setup-aiken@v0.1.0 15 | with: 16 | version: v1.0.26-alpha 17 | 18 | - run: aiken fmt --check 19 | - run: aiken check -D 20 | - run: aiken build 21 | -------------------------------------------------------------------------------- /lib/course/types.ak: -------------------------------------------------------------------------------- 1 | pub type MarketDatum { 2 | price: Int, 3 | seller: ByteArray, 4 | } 5 | 6 | pub type MarketAction { 7 | MBuy 8 | MEdit { price: Int } 9 | MDelist 10 | } 11 | 12 | pub type OracleDatum { 13 | exchange: Int, 14 | timestamp: Int, 15 | currency: ByteArray, 16 | } 17 | 18 | pub type OracleAction { 19 | OUpdate { exchange: Int, timestamp: Int } 20 | OClose 21 | } 22 | 23 | pub type OracleMintAction { 24 | OMint { exchange: Int, timestamp: Int, currency: ByteArray } 25 | OBurn 26 | } 27 | -------------------------------------------------------------------------------- /validators/b/oneshot.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/dict 2 | use aiken/collection/list 3 | use aiken/collection/pairs 4 | use cardano/assets.{PolicyId} 5 | use cardano/transaction.{OutputReference, Transaction} 6 | 7 | // Oneshot Minting Policy // 8 | pub type MintAction { 9 | OneMint 10 | OneBurn 11 | } 12 | 13 | // Can only be minted once 14 | validator oneshot(oref: OutputReference) { 15 | mint(r: MintAction, p: PolicyId, tx: Transaction) { 16 | let Transaction { inputs, mint, .. } = tx 17 | 18 | expect [Pair(tokenName, tokenQty)] = 19 | mint 20 | |> assets.tokens(p) 21 | |> dict.to_pairs() 22 | 23 | when r is { 24 | OneMint -> { 25 | expect Some(in) = 26 | list.find(inputs, fn(input) { input.output_reference == oref }) 27 | and { 28 | tokenName == "", 29 | tokenQty == 1, 30 | } 31 | } 32 | OneBurn -> tokenQty == -1 33 | } 34 | } 35 | 36 | else(_) { 37 | fail 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /validators/always/always.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/list 2 | use cardano/address.{Credential, Script} 3 | use cardano/assets.{PolicyId} 4 | use cardano/transaction.{Datum, OutputReference, Transaction, Redeemer} 5 | 6 | validator always { 7 | mint(_r: Redeemer, _p: PolicyId, _tx: Transaction) -> Bool { 8 | True 9 | } 10 | 11 | spend(d: Option, _r: Redeemer, _oref: OutputReference, _tx: Transaction) -> Bool { 12 | when d is { 13 | Some(_datum) -> True 14 | None -> False 15 | } 16 | } 17 | 18 | else(_) { 19 | fail 20 | } 21 | } 22 | 23 | validator alwaysW { 24 | withdraw(_r: Redeemer, _c: Credential, _tx: Transaction) -> Bool { 25 | True 26 | } 27 | 28 | else(_) { 29 | fail 30 | } 31 | } 32 | 33 | validator vestingTokens { 34 | mint(_r: Redeemer, p: PolicyId, tx: Transaction) -> Bool { 35 | let Transaction { outputs, .. } = tx 36 | 37 | expect Some(_ownOut) = 38 | list.find( 39 | outputs, 40 | fn( output ) { output.address.payment_credential == Script(p) } 41 | ) 42 | 43 | True 44 | } 45 | 46 | spend(_d: Option, _r: Redeemer, _oref: OutputReference, _tx: Transaction) -> Bool { 47 | True 48 | } 49 | } -------------------------------------------------------------------------------- /lib/course/tests.ak: -------------------------------------------------------------------------------- 1 | use cardano/address.{Address, Script, VerificationKey} 2 | use cardano/assets.{Value} 3 | use cardano/transaction.{Datum, Input, Output, OutputReference} 4 | 5 | pub fn input(oref: OutputReference, output: Output) -> Input { 6 | let input = Input { output_reference: oref, output } 7 | 8 | input 9 | } 10 | 11 | pub fn oref(id: ByteArray, index: Int) -> OutputReference { 12 | let oref = OutputReference { transaction_id: id, output_index: index } 13 | oref 14 | } 15 | 16 | pub fn output(addr: Address, value: Value, datum: Datum) -> Output { 17 | let output = Output { address: addr, value, datum, reference_script: None } 18 | 19 | output 20 | } 21 | 22 | pub fn scriptAddress(hash: ByteArray) -> Address { 23 | let address = 24 | Address { payment_credential: Script(hash), stake_credential: None } 25 | 26 | address 27 | } 28 | 29 | pub fn walletAddress(hash: ByteArray) -> Address { 30 | let address = 31 | Address { 32 | payment_credential: VerificationKey(hash), 33 | stake_credential: None, 34 | } 35 | 36 | address 37 | } 38 | 39 | pub fn makeAsset( 40 | policyId: ByteArray, 41 | assetName: ByteArray, 42 | quantity: Int, 43 | ) -> Value { 44 | let asset = assets.from_asset(policyId, assetName, quantity) 45 | 46 | asset 47 | } 48 | -------------------------------------------------------------------------------- /validators/vulnerabilities/vuln-tests.ak: -------------------------------------------------------------------------------- 1 | // // 2 | // Vulnerability Tests // 3 | // // 4 | 5 | // Denial Of Service // 6 | 7 | // Dust Attack - See dust-attack.ak 8 | 9 | // // Datum Attack 10 | // pub type VulnDatum { 11 | // a: Int, // is 100 acceptable? is 99999999999999999999999999999999999+ acceptable? 12 | // b: ByteArray, // #"face" may be fine, but what about "098171234098743781201239458656754918761208478605978126346759048761098761238457679871623498576675348919028734656745923918763247599345871690873245905876203897450981239487569817634756998476531792837465"? 13 | // c: List // [hash1, hash2, hash3, hash4] is probably alright, but what happens with 1000 hashes? 14 | // } 15 | // // this will be fine 16 | // pub const goodDatum { 17 | // a: 100, 18 | // b: #"cafe", 19 | // c: [#"face", #"cafe", #"f00d"] 20 | // } 21 | // // this may be possible, and can result in an unspendable utxo 22 | // pub const unspendableDatum { 23 | // a: 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999, 24 | // b: "some crazy amount of digits that result in an unspendable utxo because the datum is too large to consume. This example might not be enough, but when you have lots of PloicyIDs or ScriptHashes being added to a datum, you will quickly result in an unspendable utxo, particularly if it is being used in the input and output, because it is automatically taking up double the space", 25 | // c: [hash1, hash2, hash3, hash4, hash5, hash6, hash7, hash8, hash9, hash10], 26 | // } 27 | 28 | // Transaction Concurrency Attack 29 | 30 | // Unauthorised Data Modification // 31 | 32 | // Other Redeemer -> not checking tx state properly 33 | // Stake Attack -> see stake-attack.ak 34 | 35 | // Protocol Leaks // 36 | 37 | // Token Swap 38 | // Infinite Mint -> see infinite-mint.ak && policyMultiMint in oracle-test.ak 39 | // Double Satisfaction -> see double-satisfaction.ak 40 | -------------------------------------------------------------------------------- /validators/vulnerabilities/dust-attack.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/list 2 | use cardano/assets.{PolicyId} 3 | use cardano/transaction.{ 4 | Datum, InlineDatum, OutputReference, Redeemer, Transaction, placeholder, 5 | } 6 | use course/tests as t 7 | 8 | // The goal of a dust attack, similar to a datum attack, is to leave the protocol with 9 | // an unspendable UTxO 10 | 11 | // HOMEWORK: Try and adjust this validator to prevent the attack test from passing 12 | 13 | validator dust(p: PolicyId) { 14 | spend(_d: Option, _r: Redeemer, oref: OutputReference, tx: Transaction) { 15 | expect Some(ownIn) = 16 | list.find( 17 | tx.inputs, 18 | fn(input) { 19 | input.output_reference == oref && list.has( 20 | assets.policies(input.output.value), 21 | p, 22 | ) 23 | }, 24 | ) 25 | 26 | expect Some(ownOut) = 27 | list.find( 28 | tx.outputs, 29 | fn(output) { list.has(assets.policies(output.value), p) }, 30 | ) 31 | 32 | assets.lovelace_of(ownOut.value) >= 2000000 33 | } 34 | 35 | else(_) { 36 | fail 37 | } 38 | } 39 | 40 | test dustAttack() { 41 | let policy = #"0000" 42 | let oref = t.oref(#"cece", 1) 43 | let datum = #"aaaaaa" 44 | let redeemer = #"bbbbbb" 45 | 46 | let mainTokenValue = t.makeAsset(policy, #"1111", 1) 47 | let tokenMin = assets.merge(assets.from_lovelace(2000000), mainTokenValue) 48 | 49 | let dustTokens = 50 | t.makeAsset(#"0101", #"", 100) 51 | |> assets.merge(t.makeAsset(#"0202", #"0202", 234)) 52 | |> assets.merge(t.makeAsset(#"0303", #"0303", 234)) 53 | |> assets.merge(t.makeAsset(#"0404", #"0404", 234)) 54 | |> assets.merge(t.makeAsset(#"0505", #"0505", 234)) 55 | |> assets.merge(t.makeAsset(#"0606", #"0606", 234)) 56 | |> assets.merge(t.makeAsset(#"0707", #"0707", 234)) 57 | |> assets.merge(t.makeAsset(#"0808", #"0808", 234)) 58 | |> assets.merge(t.makeAsset(#"0909", #"0909", 234)) 59 | 60 | let input = 61 | t.input( 62 | oref, 63 | t.output(t.scriptAddress(#"face"), tokenMin, InlineDatum(datum)), 64 | ) 65 | 66 | let output = 67 | t.output( 68 | t.scriptAddress(#"face"), 69 | assets.merge(tokenMin, dustTokens), 70 | InlineDatum(datum), 71 | ) 72 | 73 | let tx = Transaction { ..placeholder, inputs: [input], outputs: [output] } 74 | 75 | dust.spend(policy, Some(InlineDatum(datum)), redeemer, oref, tx) 76 | } 77 | -------------------------------------------------------------------------------- /validators/vulnerabilities/stake-attack.ak: -------------------------------------------------------------------------------- 1 | use a/market 2 | use cardano/address.{Address, Inline, Script, VerificationKey} 3 | use cardano/assets.{AssetName, PolicyId} 4 | use cardano/transaction.{ 5 | InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, placeholder, 6 | } 7 | use course/types.{MBuy, MDelist, MEdit, MarketDatum} 8 | 9 | // // 10 | // Stake Attack Test // 11 | // // 12 | 13 | // HOMEWORK: Adjust the market validator to prevent a stake attack 14 | // This test should fail, but the other tests should still pass (market-test.ak) 15 | 16 | test marketEdit() { 17 | let seller = #"cafe" 18 | 19 | let tokenPolicy: PolicyId = #"dead" 20 | let tokenName: AssetName = #"beef" 21 | 22 | let marketDatum = MarketDatum { price: 200, seller } 23 | let marketDatum2 = MarketDatum { price: 300, seller } 24 | let marketAction = MEdit { price: 300 } 25 | 26 | let assetValue = assets.from_asset(tokenPolicy, tokenName, 1) 27 | 28 | let marketValue = assets.merge(assets.from_lovelace(2), assetValue) 29 | 30 | let sellerIn = 31 | Input { 32 | output_reference: OutputReference { 33 | transaction_id: seller, 34 | output_index: 4, 35 | }, 36 | output: Output { 37 | address: Address { 38 | payment_credential: VerificationKey(seller), 39 | stake_credential: None, 40 | }, 41 | value: assets.from_lovelace(200), 42 | datum: NoDatum, 43 | reference_script: None, 44 | }, 45 | } 46 | 47 | let oref = OutputReference { transaction_id: #"cece", output_index: 1 } 48 | 49 | let marketIn = 50 | Input { 51 | output_reference: oref, 52 | output: Output { 53 | address: Address { 54 | payment_credential: Script(#"deaf"), 55 | stake_credential: None, 56 | }, 57 | value: marketValue, 58 | datum: InlineDatum(marketDatum), 59 | reference_script: None, 60 | }, 61 | } 62 | 63 | let marketOut = 64 | Output { 65 | address: Address { 66 | payment_credential: Script(#"deaf"), 67 | stake_credential: Some(Inline(VerificationKey(#"face"))), 68 | }, 69 | value: marketValue, 70 | datum: InlineDatum(marketDatum2), 71 | reference_script: None, 72 | } 73 | 74 | let tx = 75 | Transaction { 76 | ..placeholder, 77 | inputs: [sellerIn, marketIn], 78 | outputs: [marketOut], 79 | extra_signatories: [seller], 80 | } 81 | 82 | market.market.spend(Some(marketDatum), marketAction, oref, tx)? 83 | } 84 | -------------------------------------------------------------------------------- /validators/a/market.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/list 2 | use aiken/primitive/string 3 | use cardano/address.{VerificationKey} 4 | use cardano/assets 5 | use cardano/transaction.{InlineDatum, OutputReference, Transaction} 6 | use course/types.{MBuy, MDelist, MEdit, MarketAction, MarketDatum} 7 | 8 | // Market Validator Module 1 // 9 | 10 | validator market { 11 | spend( 12 | datum: Option, 13 | r: MarketAction, 14 | oref: OutputReference, 15 | tx: Transaction, 16 | ) { 17 | // Sell assets for fixed price ADA 18 | // Seller can remove assets ( Delist ) 19 | // Seller can update listing ( Edit ) 20 | // Anyone can buy if they pay the seller ( Buy ) 21 | trace @"Spending UTxO ..." 22 | 23 | expect Some(d) = datum 24 | 25 | when r is { 26 | MBuy -> { 27 | // seller gets paid the listed amount 28 | expect Some(payment) = 29 | // output to seller address 30 | list.find( 31 | tx.outputs, 32 | fn(output) { 33 | output.address.payment_credential == VerificationKey(d.seller) 34 | }, 35 | ) 36 | 37 | trace string.concat( 38 | @"Buying asset for: ", 39 | string.from_int(assets.lovelace_of(payment.value)), 40 | ) 41 | 42 | // payment value is correct 43 | assets.lovelace_of(payment.value) == d.price 44 | } 45 | MEdit { price } -> { 46 | // change MarketDatum.price -> return to marketplace 47 | expect Some(ownInput) = 48 | list.find(tx.inputs, fn(input) { input.output_reference == oref }) 49 | 50 | let ownHash = ownInput.output.address.payment_credential 51 | 52 | // script Output has same Value as input 53 | expect Some(ownOutput) = 54 | list.find( 55 | tx.outputs, 56 | fn(output) { 57 | output.address.payment_credential == ownHash && output.value == ownInput.output.value 58 | }, 59 | ) 60 | 61 | // check output Datum is MarketDatum 62 | expect InlineDatum(datum) = ownOutput.datum 63 | expect newDatum: MarketDatum = datum 64 | 65 | trace string.concat( 66 | @"Editing price to: ", 67 | string.from_int(newDatum.price), 68 | ) 69 | 70 | // check newDatum Values && signature 71 | and { 72 | newDatum.seller == d.seller, 73 | newDatum.price == price, 74 | list.has(tx.extra_signatories, d.seller), 75 | } 76 | } 77 | MDelist -> { 78 | trace @"Delisting asset" 79 | // Only if signed by Seller 80 | list.has(tx.extra_signatories, d.seller)? 81 | } 82 | } 83 | } 84 | 85 | else(_) { 86 | fail 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aiken Course 2 | 3 | This is the Aiken Course! Empowered by the Cardano Foundation 4 | 5 | In this course we are going to be taking budding smart contract developers on a journey from basic validators through to complex dapps with multi validators, optimisations and TESTS! 6 | 7 | Its going to be a smooth & casual ride that will introduce concepts as we develop our validators, to carefully guide people through the complexities of the eUTxO model and how to think about systems used for constructing production ready Dapps. 8 | 9 | ## Course Material: 10 | 11 | Introduction - Setup & Using Aiken 12 | 13 | Firstly we walk through setting up Aiken and the basecs or writing validators 14 | -> Installation 15 | -> Project Organistion 16 | -> Writing Validators 17 | 18 | Module 1 - Basic Marketplace 19 | 20 | This is going to be a single validator to introduce Aiken and basic data types in validators 21 | -> I/O 22 | -> Datums 23 | -> Redeemers 24 | -> Signatories / Wallet && Script Credentials 25 | 26 | Module 2 - Testing Validators 27 | 28 | We look at writing tests for our validators to benchmark and understand how the transactions will be built 29 | -> Unit Tests 30 | -> Passing Tests for Redeemer Cases 31 | -> Test tools helper functions 32 | -> Failing Tests 33 | 34 | Module 3 - Oracle 35 | 36 | This will teach people the most common design pattern of a mintingPolicy & spendingValidator pair 37 | -> Trusted data sources 38 | -> Spending to yourself 39 | -> Oneshot minting policy pattern 40 | 41 | Module 4 - Common Exploits 42 | 43 | This Module will walk through some common exploits we need to protect against 44 | -> Bypassing checks 45 | -> Double Satisfaction 46 | -> Token Swap 47 | -> Stake Injection 48 | -> Junk Tokens 49 | -> Datum Validity 50 | 51 | Module 3 - USD Marketplace 52 | 53 | Using our oracle in our marketplace to charge in 'USD' 54 | -> Refernce Inputs 55 | -> checks against multiple I/Os 56 | 57 | Module 4 - CIP68 Token Minting 58 | 59 | Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. 60 | 61 | For example, as `validators/always_true.ak` 62 | 63 | --- 64 | 65 | # Working With Aiken 66 | 67 | ```aiken 68 | validator my_first_validator { 69 | spend(_datum: Option, _redeemer: Data, _output_reference: Data, _context: Data) { 70 | True 71 | } 72 | } 73 | ``` 74 | 75 | ## Building 76 | 77 | ```sh 78 | aiken build 79 | ``` 80 | 81 | ## Testing 82 | 83 | You can write tests in any module using the `test` keyword. For example: 84 | 85 | ```aiken 86 | test foo() { 87 | 1 + 1 == 2 88 | } 89 | ``` 90 | 91 | To run all tests, simply do: 92 | 93 | ```sh 94 | aiken check 95 | ``` 96 | 97 | To run only tests matching the string `foo`, do: 98 | 99 | ```sh 100 | aiken check -m foo 101 | ``` 102 | 103 | ## Documentation 104 | 105 | If you're writing a library, you might want to generate an HTML documentation for it. 106 | 107 | Use: 108 | 109 | ```sh 110 | aiken docs 111 | ``` 112 | 113 | ## Resources 114 | 115 | Find more on the [Aiken's user manual](https://aiken-lang.org). 116 | -------------------------------------------------------------------------------- /validators/a/market-test.ak: -------------------------------------------------------------------------------- 1 | use a/market 2 | use cardano/assets 3 | use cardano/transaction.{InlineDatum, NoDatum, Transaction, placeholder} 4 | use course/tests as t 5 | use course/types.{MBuy, MDelist, MEdit, MarketDatum} 6 | 7 | // Market Tests // 8 | 9 | test marketBuy() { 10 | let seller = #"face" 11 | let buyer = #"beef" 12 | 13 | let marketDatum = MarketDatum { price: 200, seller } 14 | let marketAction = MBuy 15 | 16 | let marketValue = 17 | assets.merge(assets.from_lovelace(2), t.makeAsset(#"dead", #"feed", 1)) 18 | 19 | let oref = t.oref(#"cafe", 1) 20 | 21 | let marketIn = 22 | t.input( 23 | oref, 24 | t.output(t.scriptAddress(#"deaf"), marketValue, InlineDatum(marketDatum)), 25 | ) 26 | 27 | let buyerIn = 28 | t.input( 29 | t.oref(#"cece", 2), 30 | t.output(t.walletAddress(buyer), assets.from_lovelace(200), NoDatum), 31 | ) 32 | 33 | let buyerOut = t.output(t.walletAddress(buyer), marketValue, NoDatum) 34 | 35 | let sellerOut = 36 | t.output(t.walletAddress(seller), assets.from_lovelace(200), NoDatum) 37 | 38 | let tx = 39 | Transaction { 40 | ..placeholder, 41 | inputs: [buyerIn, marketIn], 42 | outputs: [buyerOut, sellerOut], 43 | } 44 | 45 | market.market.spend(Some(marketDatum), marketAction, oref, tx) 46 | } 47 | 48 | test marketEdit() { 49 | let seller = #"face" 50 | 51 | let marketDatum = MarketDatum { price: 200, seller } 52 | let marketAction = MEdit { price: 250 } 53 | 54 | let newDatum = MarketDatum { price: 250, seller } 55 | 56 | let marketValue = 57 | assets.merge(assets.from_lovelace(2), t.makeAsset(#"dead", #"feed", 1)) 58 | 59 | let oref = t.oref(#"cafe", 1) 60 | 61 | let marketIn = 62 | t.input( 63 | oref, 64 | t.output(t.scriptAddress(#"deaf"), marketValue, InlineDatum(marketDatum)), 65 | ) 66 | 67 | let marketOut = 68 | t.output(t.scriptAddress(#"deaf"), marketValue, InlineDatum(newDatum)) 69 | 70 | let tx = 71 | Transaction { 72 | ..placeholder, 73 | inputs: [marketIn], 74 | outputs: [marketOut], 75 | extra_signatories: [seller], 76 | } 77 | 78 | market.market.spend(Some(marketDatum), marketAction, oref, tx) 79 | } 80 | 81 | test marketDelist() { 82 | let seller = #"face" 83 | 84 | let marketDatum = MarketDatum { price: 200, seller } 85 | let marketAction = MDelist 86 | 87 | let marketValue = 88 | assets.merge(assets.from_lovelace(2), t.makeAsset(#"dead", #"feed", 1)) 89 | 90 | let oref = t.oref(#"cafe", 1) 91 | 92 | let marketIn = 93 | t.input( 94 | oref, 95 | t.output(t.scriptAddress(#"deaf"), marketValue, InlineDatum(marketDatum)), 96 | ) 97 | 98 | let sellerOut = t.output(t.walletAddress(seller), marketValue, NoDatum) 99 | 100 | let tx = 101 | Transaction { 102 | ..placeholder, 103 | inputs: [marketIn], 104 | outputs: [sellerOut], 105 | extra_signatories: [seller], 106 | } 107 | 108 | market.market.spend(Some(marketDatum), marketAction, oref, tx) 109 | } 110 | 111 | test marketDelistFail() fail { 112 | let seller = #"face" 113 | let buyer = #"beef" 114 | 115 | let marketDatum = MarketDatum { price: 200, seller } 116 | let marketAction = MDelist 117 | 118 | let marketValue = 119 | assets.merge(assets.from_lovelace(2), t.makeAsset(#"dead", #"feed", 1)) 120 | 121 | let oref = t.oref(#"cafe", 1) 122 | 123 | let marketIn = 124 | t.input( 125 | oref, 126 | t.output(t.scriptAddress(#"deaf"), marketValue, InlineDatum(marketDatum)), 127 | ) 128 | 129 | let sellerOut = t.output(t.walletAddress(seller), marketValue, NoDatum) 130 | 131 | let tx = 132 | Transaction { 133 | ..placeholder, 134 | inputs: [marketIn], 135 | outputs: [sellerOut], 136 | extra_signatories: [buyer], 137 | } 138 | 139 | market.market.spend(Some(marketDatum), marketAction, oref, tx)? 140 | } 141 | -------------------------------------------------------------------------------- /validators/vulnerabilities/double-satisfaction.ak: -------------------------------------------------------------------------------- 1 | use a/market 2 | use cardano/address.{Address, Script, VerificationKey} 3 | use cardano/assets.{AssetName, PolicyId} 4 | use cardano/transaction.{ 5 | InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, placeholder, 6 | } 7 | use course/types.{MBuy, MDelist, MEdit, MarketDatum} 8 | 9 | // // 10 | // Double Satisfaction Test // 11 | // // 12 | 13 | // enables a single utxo to satisfy multiple validator checks, enabling someone to 14 | // withdraw more than is intended by the validator 15 | 16 | // HOMEWORK: prevent this test from passing whilst still passing the main tests in 17 | // market-test.ak 18 | 19 | test marketplaceBuy() { 20 | let buyer = #"face" 21 | let seller = #"cafe" 22 | 23 | let oref = OutputReference { transaction_id: #"cece", output_index: 1 } 24 | let vref = OutputReference { transaction_id: #"cece", output_index: 2 } 25 | 26 | let tokenPolicy: PolicyId = #"dead" 27 | let tokenName: AssetName = #"beef" 28 | 29 | let vulnToken = assets.from_asset(#"deaf", #"feed", 1) 30 | 31 | let marketDatum = MarketDatum { price: 200, seller } 32 | let marketAction = MBuy 33 | 34 | let assetValue = assets.from_asset(tokenPolicy, tokenName, 1) 35 | 36 | let marketValue = assets.merge(assets.from_lovelace(2), assetValue) 37 | 38 | let buyerIn = 39 | Input { 40 | output_reference: OutputReference { 41 | transaction_id: buyer, 42 | output_index: 4, 43 | }, 44 | output: Output { 45 | address: Address { 46 | payment_credential: VerificationKey(buyer), 47 | stake_credential: None, 48 | }, 49 | value: assets.from_lovelace(200), 50 | datum: NoDatum, 51 | reference_script: None, 52 | }, 53 | } 54 | 55 | let marketIn = 56 | // Legit spending 57 | Input { 58 | output_reference: oref, 59 | output: Output { 60 | address: Address { 61 | payment_credential: Script(#"deaf"), 62 | stake_credential: None, 63 | }, 64 | value: marketValue, 65 | datum: InlineDatum(marketDatum), 66 | reference_script: None, 67 | }, 68 | } 69 | 70 | let vulnIn = 71 | // naughty spending 72 | Input { 73 | output_reference: vref, 74 | output: Output { 75 | address: Address { 76 | payment_credential: Script(#"deaf"), 77 | stake_credential: None, 78 | }, 79 | value: vulnToken, 80 | datum: InlineDatum(marketDatum), 81 | reference_script: None, 82 | }, 83 | } 84 | 85 | let buyerOut = 86 | Output { 87 | address: Address { 88 | payment_credential: VerificationKey(buyer), 89 | stake_credential: None, 90 | }, 91 | value: marketValue, 92 | datum: NoDatum, 93 | reference_script: None, 94 | } 95 | 96 | let vulnOut = 97 | // exploited withdrawl of token/utxo 98 | Output { 99 | address: Address { 100 | payment_credential: VerificationKey(buyer), 101 | stake_credential: None, 102 | }, 103 | value: vulnToken, 104 | datum: NoDatum, 105 | reference_script: None, 106 | } 107 | 108 | let sellerOut = 109 | Output { 110 | address: Address { 111 | payment_credential: VerificationKey(seller), 112 | stake_credential: None, 113 | }, 114 | value: assets.from_lovelace(200), 115 | datum: NoDatum, 116 | reference_script: None, 117 | } 118 | 119 | let tx = 120 | Transaction { 121 | ..placeholder, 122 | inputs: [buyerIn, marketIn, vulnIn], 123 | outputs: [buyerOut, sellerOut, vulnOut], 124 | } 125 | 126 | and { 127 | market.market.spend(Some(marketDatum), marketAction, oref, tx)?, 128 | market.market.spend(Some(marketDatum), marketAction, vref, tx)?, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /validators/b/oracle.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/dict 2 | use aiken/collection/list 3 | use aiken/collection/pairs 4 | use cardano/address.{Script} 5 | use cardano/assets.{PolicyId} 6 | use cardano/transaction.{InlineDatum, OutputReference, Transaction} 7 | use course/types.{ 8 | OBurn, OClose, OMint, OUpdate, OracleAction, OracleDatum, OracleMintAction, 9 | } 10 | 11 | // Oracle Validator // 12 | 13 | // HOMEWORK: can you exploit this? 14 | // write failing tests for this oracle validator -> 1 for each case 15 | 16 | // Trusted Data Feed 17 | validator oracle(oneshot: OutputReference) { 18 | mint(r: OracleMintAction, p: PolicyId, tx: Transaction) { 19 | let Transaction { inputs, outputs, mint, .. } = tx 20 | 21 | expect [Pair(tokenName, tokenQty)] = 22 | mint 23 | |> assets.tokens(p) 24 | |> dict.to_pairs() 25 | 26 | when r is { 27 | OMint { exchange, timestamp, currency } -> { 28 | expect Some(in) = 29 | list.find(inputs, fn(input) { input.output_reference == oneshot }) 30 | 31 | // Output With Minted Token! 32 | expect Some(tokenOut) = 33 | list.find( 34 | outputs, 35 | fn(output) { 36 | assets.without_lovelace(output.value) == assets.from_asset( 37 | p, 38 | tokenName, 39 | tokenQty, 40 | ) 41 | }, 42 | ) 43 | 44 | // Needs to send to trusted validator 45 | expect Script(p) == tokenOut.address.payment_credential 46 | 47 | // Needs to have OracleDatum 48 | expect InlineDatum(d) = tokenOut.datum 49 | expect oracleDatum: OracleDatum = d 50 | 51 | and { 52 | tokenName == "", 53 | tokenQty == 1, 54 | oracleDatum.exchange == exchange, 55 | oracleDatum.timestamp == timestamp, 56 | oracleDatum.currency == currency, 57 | } 58 | } 59 | OBurn -> tokenQty == -1 60 | } 61 | } 62 | 63 | spend( 64 | datum: Option, 65 | r: OracleAction, 66 | oref: OutputReference, 67 | tx: Transaction, 68 | ) { 69 | let Transaction { inputs, outputs, mint, .. } = tx 70 | // What we need: 71 | // own ScriptHash for Policy Id of Token 72 | // own Input for oracle token value 73 | // input has datum 74 | expect Some(d) = datum 75 | // find own input 76 | expect Some(ownInput) = 77 | list.find(inputs, fn(input) { input.output_reference == oref }) 78 | 79 | // get script hash from credential 80 | let ownCredential = ownInput.output.address.payment_credential 81 | expect Script(scriptHash) = ownCredential 82 | 83 | when r is { 84 | // To Update : 85 | OUpdate { exchange, timestamp } -> { 86 | // find own output with token && returned to validator 87 | expect Some(ownOut) = 88 | list.find( 89 | outputs, 90 | fn(output) { 91 | list.has(assets.policies(output.value), scriptHash) && output.address.payment_credential == ownCredential 92 | }, 93 | ) 94 | // check correct datum 95 | expect InlineDatum(dat) = ownOut.datum 96 | expect outDatum: OracleDatum = dat 97 | 98 | and { 99 | // new exchange && timestamp 100 | outDatum.exchange == exchange, 101 | outDatum.currency == d.currency, 102 | outDatum.timestamp == timestamp, 103 | timestamp > d.timestamp, 104 | } 105 | } 106 | 107 | // To Close: 108 | // token is burned 109 | OClose -> { 110 | // get input token 111 | let oracleValue = 112 | ownInput.output.value 113 | |> assets.tokens(scriptHash) 114 | |> dict.to_pairs() 115 | 116 | // filter mint field 117 | let burnValue = 118 | mint 119 | |> assets.tokens(scriptHash) 120 | |> dict.to_pairs() 121 | 122 | // value is burned 123 | and { 124 | pairs.keys(burnValue) == pairs.keys(oracleValue), 125 | pairs.values(burnValue) == [-1], 126 | pairs.values(oracleValue) == [1], 127 | } 128 | } 129 | } 130 | } 131 | 132 | else(_) { 133 | fail 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /validators/vulnerabilities/infinite-mint.ak: -------------------------------------------------------------------------------- 1 | use aiken/collection/dict 2 | use aiken/collection/list 3 | use aiken/collection/pairs 4 | use cardano/address.{Script} 5 | use cardano/assets.{PolicyId} 6 | use cardano/transaction.{ 7 | InlineDatum, NoDatum, OutputReference, Transaction, placeholder, 8 | } 9 | use course/tests as t 10 | use course/types.{ 11 | OBurn, OClose, OMint, OUpdate, OracleAction, OracleDatum, OracleMintAction, 12 | } 13 | 14 | // Oracle Validator // 15 | 16 | // HOMEWORK: can you exploit this? 17 | // write failing tests for this oracle validator -> 1 for each case 18 | 19 | // Trusted Data Feed 20 | validator oracle(oneshot: OutputReference) { 21 | mint(r: OracleMintAction, p: PolicyId, tx: Transaction) { 22 | let Transaction { inputs, outputs, mint, .. } = tx 23 | 24 | let oracleMint = 25 | mint 26 | |> assets.tokens(p) 27 | |> dict.to_pairs() 28 | 29 | when r is { 30 | OMint { exchange, timestamp, currency } -> { 31 | expect Some(in) = 32 | list.find(inputs, fn(input) { input.output_reference == oneshot }) 33 | 34 | // Output With Minted Token! 35 | expect Some(tokenOut) = 36 | list.find( 37 | outputs, 38 | fn(output) { list.has(assets.policies(output.value), p) }, 39 | ) 40 | 41 | // Needs to send to trusted validator 42 | expect Script(p) == tokenOut.address.payment_credential 43 | 44 | // Needs to have OracleDatum 45 | expect InlineDatum(d) = tokenOut.datum 46 | expect oracleDatum: OracleDatum = d 47 | 48 | and { 49 | // tokenName == "", 50 | // tokenQty == 1, 51 | oracleDatum.exchange == exchange, 52 | oracleDatum.timestamp == timestamp, 53 | oracleDatum.currency == currency, 54 | } 55 | } 56 | OBurn -> True 57 | } 58 | } 59 | 60 | spend( 61 | datum: Option, 62 | r: OracleAction, 63 | oref: OutputReference, 64 | tx: Transaction, 65 | ) { 66 | True 67 | } 68 | 69 | else(_) { 70 | fail 71 | } 72 | } 73 | 74 | // Infinite Mint Test // 75 | 76 | // Oracle Mint 77 | test tokenMultiMint() { 78 | let exchange = 1000 79 | let timestamp = 1111111111111 80 | let currency = "USD" 81 | 82 | let oref = t.oref(#"face", 1) 83 | let oraclePolicy = #"dead" 84 | let oracleToken = "" 85 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 100) 86 | 87 | let oracleMintAction = OMint { exchange, timestamp, currency } 88 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 89 | 90 | let oneshot = 91 | t.input( 92 | oref, 93 | t.output(t.walletAddress(#"bace"), assets.from_lovelace(100), NoDatum), 94 | ) 95 | 96 | let oracleOut = 97 | t.output( 98 | t.scriptAddress(oraclePolicy), 99 | oracleValue, 100 | InlineDatum(oracleDatum), 101 | ) 102 | 103 | let tx = 104 | Transaction { 105 | ..placeholder, 106 | inputs: [oneshot], 107 | outputs: [oracleOut], 108 | mint: oracleValue, 109 | } 110 | 111 | oracle.mint(oref, oracleMintAction, oraclePolicy, tx)? 112 | } 113 | 114 | test policyMultiMint() { 115 | let exchange = 1000 116 | let timestamp = 1111111111111 117 | let currency = "USD" 118 | 119 | let oref = t.oref(#"face", 1) 120 | let oraclePolicy = #"dead" 121 | let oracleToken = "" 122 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 100) 123 | 124 | let dustTokens = 125 | t.makeAsset(oraclePolicy, #"1111", 100) 126 | |> assets.merge(t.makeAsset(oraclePolicy, #"0202", 234)) 127 | |> assets.merge(t.makeAsset(oraclePolicy, #"0303", 234)) 128 | |> assets.merge(t.makeAsset(oraclePolicy, #"0404", 234)) 129 | |> assets.merge(t.makeAsset(oraclePolicy, #"0505", 234)) 130 | |> assets.merge(t.makeAsset(oraclePolicy, #"0606", 234)) 131 | |> assets.merge(t.makeAsset(oraclePolicy, #"0707", 234)) 132 | |> assets.merge(t.makeAsset(oraclePolicy, #"0808", 234)) 133 | |> assets.merge(t.makeAsset(oraclePolicy, #"0909", 234)) 134 | 135 | let oracleMintAction = OMint { exchange, timestamp, currency } 136 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 137 | 138 | let oneshot = 139 | t.input( 140 | oref, 141 | t.output(t.walletAddress(#"bace"), assets.from_lovelace(100), NoDatum), 142 | ) 143 | 144 | let oracleOut = 145 | t.output( 146 | t.scriptAddress(oraclePolicy), 147 | assets.merge(oracleValue, dustTokens), 148 | InlineDatum(oracleDatum), 149 | ) 150 | 151 | let tx = 152 | Transaction { 153 | ..placeholder, 154 | inputs: [oneshot], 155 | outputs: [oracleOut], 156 | mint: assets.merge(oracleValue, dustTokens), 157 | } 158 | 159 | oracle.mint(oref, oracleMintAction, oraclePolicy, tx)? 160 | } 161 | -------------------------------------------------------------------------------- /validators/b/oracle-test.ak: -------------------------------------------------------------------------------- 1 | use b/oracle 2 | use cardano/assets 3 | use cardano/transaction.{InlineDatum, NoDatum, Transaction, placeholder} 4 | use course/tests as t 5 | use course/types.{OBurn, OClose, OMint, OUpdate, OracleDatum} 6 | 7 | // Oracle Validator Tests // 8 | 9 | // Oracle Update 10 | test oracleUpdate() { 11 | let exchange = 1000 12 | let timestamp = 1111111111112 13 | let currency = "USD" 14 | 15 | let oneshot = t.oref(#"face", 1) 16 | let oraclePolicy = #"dead" 17 | let oracleToken = "" 18 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 1) 19 | 20 | let oref = t.oref(#"cafe", 1) 21 | 22 | let inDatum = 23 | OracleDatum { exchange: 500, timestamp: 1111111111111, currency } 24 | let outDatum = OracleDatum { exchange, timestamp, currency } 25 | let oracleAction = OUpdate { exchange, timestamp } 26 | 27 | let oracleIn = 28 | t.input( 29 | oref, 30 | t.output( 31 | t.scriptAddress(oraclePolicy), 32 | // matches policyId 33 | oracleValue, 34 | InlineDatum(inDatum), 35 | ), 36 | ) 37 | 38 | let oracleOut = 39 | t.output(t.scriptAddress(oraclePolicy), oracleValue, InlineDatum(outDatum)) 40 | 41 | let tx = 42 | Transaction { ..placeholder, inputs: [oracleIn], outputs: [oracleOut] } 43 | 44 | oracle.oracle.spend(oneshot, Some(inDatum), oracleAction, oref, tx) 45 | } 46 | 47 | // Oracle Close 48 | test oracleClose() { 49 | let exchange = 1000 50 | let timestamp = 1111111111111 51 | // how can we limit the length of an integer? 52 | // we need to ensure that the integer is always 13digits long. 53 | let currency = "USD" 54 | 55 | let oneshot = t.oref(#"face", 1) 56 | let oref = t.oref(#"deaf", 4) 57 | let oraclePolicy = #"dead" 58 | let oracleToken = "" 59 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 1) 60 | let oracleBurnValue = t.makeAsset(oraclePolicy, oracleToken, -1) 61 | 62 | let oracleAction = OClose 63 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 64 | 65 | let oracleIn = 66 | t.input( 67 | oref, 68 | t.output( 69 | t.scriptAddress(oraclePolicy), 70 | oracleValue, 71 | InlineDatum(oracleDatum), 72 | ), 73 | ) 74 | 75 | let tx = 76 | Transaction { ..placeholder, inputs: [oracleIn], mint: oracleBurnValue } 77 | 78 | oracle.oracle.spend(oneshot, Some(oracleDatum), oracleAction, oref, tx)? 79 | } 80 | 81 | // Oracle Mint 82 | test oracleMint() { 83 | let exchange = 1000 84 | let timestamp = 1111111111111 85 | let currency = "USD" 86 | 87 | let oref = t.oref(#"face", 1) 88 | let oraclePolicy = #"dead" 89 | let oracleToken = "" 90 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 1) 91 | 92 | let oracleMintAction = OMint { exchange, timestamp, currency } 93 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 94 | 95 | let oneshot = 96 | t.input( 97 | oref, 98 | t.output(t.walletAddress(#"bace"), assets.from_lovelace(100), NoDatum), 99 | ) 100 | 101 | let oracleOut = 102 | t.output( 103 | t.scriptAddress(oraclePolicy), 104 | oracleValue, 105 | InlineDatum(oracleDatum), 106 | ) 107 | 108 | let tx = 109 | Transaction { 110 | ..placeholder, 111 | inputs: [oneshot], 112 | outputs: [oracleOut], 113 | mint: oracleValue, 114 | } 115 | 116 | oracle.oracle.mint(oref, oracleMintAction, oraclePolicy, tx)? 117 | } 118 | 119 | // Oracle Burn 120 | test oracleBurn() { 121 | let exchange = 1000 122 | let timestamp = 1111111111111 123 | let currency = "USD" 124 | 125 | let oref = t.oref(#"face", 1) 126 | let oraclePolicy = #"dead" 127 | let oracleToken = "" 128 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 1) 129 | let oracleBurnValue = t.makeAsset(oraclePolicy, oracleToken, -1) 130 | 131 | let oracleMintAction = OBurn 132 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 133 | 134 | let oracleIn = 135 | t.input( 136 | t.oref(#"deaf", 4), 137 | t.output( 138 | t.scriptAddress(oraclePolicy), 139 | oracleValue, 140 | InlineDatum(oracleDatum), 141 | ), 142 | ) 143 | 144 | let tx = 145 | Transaction { ..placeholder, inputs: [oracleIn], mint: oracleBurnValue } 146 | 147 | oracle.oracle.mint(oref, oracleMintAction, oraclePolicy, tx)? 148 | } 149 | 150 | test policyMultiMint() { 151 | let exchange = 1000 152 | let timestamp = 1111111111111 153 | let currency = "USD" 154 | 155 | let oref = t.oref(#"face", 1) 156 | let oraclePolicy = #"dead" 157 | let oracleToken = "" 158 | let oracleValue = t.makeAsset(oraclePolicy, oracleToken, 100) 159 | 160 | let dustTokens = 161 | t.makeAsset(oraclePolicy, #"1111", 100) 162 | |> assets.merge(t.makeAsset(oraclePolicy, #"0202", 234)) 163 | |> assets.merge(t.makeAsset(oraclePolicy, #"0303", 234)) 164 | |> assets.merge(t.makeAsset(oraclePolicy, #"0404", 234)) 165 | |> assets.merge(t.makeAsset(oraclePolicy, #"0505", 234)) 166 | |> assets.merge(t.makeAsset(oraclePolicy, #"0606", 234)) 167 | |> assets.merge(t.makeAsset(oraclePolicy, #"0707", 234)) 168 | |> assets.merge(t.makeAsset(oraclePolicy, #"0808", 234)) 169 | |> assets.merge(t.makeAsset(oraclePolicy, #"0909", 234)) 170 | 171 | let oracleMintAction = OMint { exchange, timestamp, currency } 172 | let oracleDatum = OracleDatum { exchange, timestamp, currency } 173 | 174 | let oneshot = 175 | t.input( 176 | oref, 177 | t.output(t.walletAddress(#"bace"), assets.from_lovelace(100), NoDatum), 178 | ) 179 | 180 | let oracleOut = 181 | t.output( 182 | t.scriptAddress(oraclePolicy), 183 | assets.merge(oracleValue, dustTokens), 184 | InlineDatum(oracleDatum), 185 | ) 186 | 187 | let tx = 188 | Transaction { 189 | ..placeholder, 190 | inputs: [oneshot], 191 | outputs: [oracleOut], 192 | mint: assets.merge(oracleValue, dustTokens), 193 | } 194 | 195 | oracle.oracle.mint(oref, oracleMintAction, oraclePolicy, tx)? 196 | } 197 | -------------------------------------------------------------------------------- /plutus.json: -------------------------------------------------------------------------------- 1 | { 2 | "preamble": { 3 | "title": "rhystmorgan/course", 4 | "description": "Aiken contracts for project 'rhystmorgan/course'", 5 | "version": "0.0.0", 6 | "plutusVersion": "v3", 7 | "compiler": { 8 | "name": "Aiken", 9 | "version": "v1.1.0+be31a7c" 10 | }, 11 | "license": "Apache-2.0" 12 | }, 13 | "validators": [ 14 | { 15 | "title": "a/market.market.spend", 16 | "datum": { 17 | "title": "datum", 18 | "schema": { 19 | "$ref": "#/definitions/course~1types~1MarketDatum" 20 | } 21 | }, 22 | "redeemer": { 23 | "title": "r", 24 | "schema": { 25 | "$ref": "#/definitions/course~1types~1MarketAction" 26 | } 27 | }, 28 | "compiledCode": "5903e2010100323232323232322533300232323232325332330083001300937540042646464646464a66601c60060022a66602260206ea8024540085854ccc038c01c0044c8c94ccc04cc0580085401058dd6980a00098081baa0091533300e3370e90020008a99980898081baa009150021616300e37540102a6660186002601a6ea80084c8c8c8c8c8c8c94ccc04cc0200184c94ccc050c024c054dd5000899b87325333015300e301637540022900009bad301a3017375400264a66602a601c602c6ea80045300103d87a8000132330010013756603660306ea8008894ccc068004530103d87a8000132323232533301b33722911000021533301b3371e9101000021300a3301f375000297ae014c0103d87a8000133006006003375a60380066eb8c068008c078008c070004c8cc004004dd59803980b9baa301a3017375400444a666032002298103d87a8000132323232533301a33722911000021533301a3371e910100002130093301e374c00297ae014c0103d87a8000133006006003375660360066eb8c064008c074008c06c004dd69802980b1baa008163300137586006602a6ea80408cdd79802980b1baa300530163754002600666030600c602c6ea80212f5c026464a66602a601c01026464a66602e601860306ea80044c8c8c94ccc068c03cc06cdd5000899299980d99b8748010c070dd5000899299980e1808980e9baa00113232323253330233026002153330203371e6eb8c048c088dd50029bae3012302237540282a66604066e1cdd6980898111baa00500b13300c3758601a60446ea8074dd7180918111baa01414a0294058dd7181200098120011bad3022001301e37540022c6040603a6ea800458c028c070dd5180f980e1baa00116330073758601260366ea805894ccc068cdd79805980e1baa300b301c3754002004266ebcc030c070dd50009806180e1baa300c301c37540062940c024c068dd51804980d1baa300a301a3754002603860326ea800458cc010dd61803980c1baa01323375e601060326ea8004038dd6980d180b9baa01013300137586004602e6ea8048dd71803980b9baa00922323300100100322533301b00114a0264a66603266e3cdd7180f0010020a51133003003001301e00123019301a301a301a301a301a301a301a301a00122323300100100322533301900114c0103d87a80001323253330183005002130073301c0024bd70099802002000980e801180d8009ba5480008c058c05cc05c0048c0540048c050c054004c038dd5004180898071baa00216370e900018079808001180700098051baa002370e90010b1805980600118050009805001180400098021baa00114984d9595cd2ab9d5573caae7d5d02ba157441", 29 | "hash": "debaf89ad244429fa75ef3b1b99193f6f6b7edeec9b19bdef85f5649" 30 | }, 31 | { 32 | "title": "a/market.market.else", 33 | "compiledCode": "5903e2010100323232323232322533300232323232325332330083001300937540042646464646464a66601c60060022a66602260206ea8024540085854ccc038c01c0044c8c94ccc04cc0580085401058dd6980a00098081baa0091533300e3370e90020008a99980898081baa009150021616300e37540102a6660186002601a6ea80084c8c8c8c8c8c8c94ccc04cc0200184c94ccc050c024c054dd5000899b87325333015300e301637540022900009bad301a3017375400264a66602a601c602c6ea80045300103d87a8000132330010013756603660306ea8008894ccc068004530103d87a8000132323232533301b33722911000021533301b3371e9101000021300a3301f375000297ae014c0103d87a8000133006006003375a60380066eb8c068008c078008c070004c8cc004004dd59803980b9baa301a3017375400444a666032002298103d87a8000132323232533301a33722911000021533301a3371e910100002130093301e374c00297ae014c0103d87a8000133006006003375660360066eb8c064008c074008c06c004dd69802980b1baa008163300137586006602a6ea80408cdd79802980b1baa300530163754002600666030600c602c6ea80212f5c026464a66602a601c01026464a66602e601860306ea80044c8c8c94ccc068c03cc06cdd5000899299980d99b8748010c070dd5000899299980e1808980e9baa00113232323253330233026002153330203371e6eb8c048c088dd50029bae3012302237540282a66604066e1cdd6980898111baa00500b13300c3758601a60446ea8074dd7180918111baa01414a0294058dd7181200098120011bad3022001301e37540022c6040603a6ea800458c028c070dd5180f980e1baa00116330073758601260366ea805894ccc068cdd79805980e1baa300b301c3754002004266ebcc030c070dd50009806180e1baa300c301c37540062940c024c068dd51804980d1baa300a301a3754002603860326ea800458cc010dd61803980c1baa01323375e601060326ea8004038dd6980d180b9baa01013300137586004602e6ea8048dd71803980b9baa00922323300100100322533301b00114a0264a66603266e3cdd7180f0010020a51133003003001301e00123019301a301a301a301a301a301a301a301a00122323300100100322533301900114c0103d87a80001323253330183005002130073301c0024bd70099802002000980e801180d8009ba5480008c058c05cc05c0048c0540048c050c054004c038dd5004180898071baa00216370e900018079808001180700098051baa002370e90010b1805980600118050009805001180400098021baa00114984d9595cd2ab9d5573caae7d5d02ba157441", 34 | "hash": "debaf89ad244429fa75ef3b1b99193f6f6b7edeec9b19bdef85f5649" 35 | }, 36 | { 37 | "title": "always/always.always.mint", 38 | "redeemer": { 39 | "title": "_r", 40 | "schema": { 41 | "$ref": "#/definitions/Redeemer" 42 | } 43 | }, 44 | "compiledCode": "587d01010032323232322533300232323232323253330083370e9000000899251375c601660146ea800854ccc020cdc3a400400226464a66601466e1d2000300b3754601a601c0042944528180600098051baa00216300837540026012601400460100026010004600c00260086ea8004526136565734aae7555cf2ba15745", 45 | "hash": "2304544873bc8ffc5b5875a7ba73ef7a5cc9c0b047d6afb28fb69d58" 46 | }, 47 | { 48 | "title": "always/always.always.spend", 49 | "datum": { 50 | "title": "d", 51 | "schema": { 52 | "$ref": "#/definitions/cardano~1transaction~1Datum" 53 | } 54 | }, 55 | "redeemer": { 56 | "title": "_r", 57 | "schema": { 58 | "$ref": "#/definitions/Redeemer" 59 | } 60 | }, 61 | "compiledCode": "587d01010032323232322533300232323232323253330083370e9000000899251375c601660146ea800854ccc020cdc3a400400226464a66601466e1d2000300b3754601a601c0042944528180600098051baa00216300837540026012601400460100026010004600c00260086ea8004526136565734aae7555cf2ba15745", 62 | "hash": "2304544873bc8ffc5b5875a7ba73ef7a5cc9c0b047d6afb28fb69d58" 63 | }, 64 | { 65 | "title": "always/always.always.else", 66 | "compiledCode": "587d01010032323232322533300232323232323253330083370e9000000899251375c601660146ea800854ccc020cdc3a400400226464a66601466e1d2000300b3754601a601c0042944528180600098051baa00216300837540026012601400460100026010004600c00260086ea8004526136565734aae7555cf2ba15745", 67 | "hash": "2304544873bc8ffc5b5875a7ba73ef7a5cc9c0b047d6afb28fb69d58" 68 | }, 69 | { 70 | "title": "always/always.alwaysW.withdraw", 71 | "redeemer": { 72 | "title": "_r", 73 | "schema": { 74 | "$ref": "#/definitions/Redeemer" 75 | } 76 | }, 77 | "compiledCode": "584e010100323232323225333002323232323253330073370e900218041baa0011324a2601460126ea800458c024c028008c020004c020008c018004c010dd50008a4c26cacae6955ceaab9e5742ae89", 78 | "hash": "77f0ba478a872b141304a48969d48c33723e93bb5b4b25998f36c650" 79 | }, 80 | { 81 | "title": "always/always.alwaysW.else", 82 | "compiledCode": "584e010100323232323225333002323232323253330073370e900218041baa0011324a2601460126ea800458c024c028008c020004c020008c018004c010dd50008a4c26cacae6955ceaab9e5742ae89", 83 | "hash": "77f0ba478a872b141304a48969d48c33723e93bb5b4b25998f36c650" 84 | }, 85 | { 86 | "title": "always/always.vestingTokens.mint", 87 | "redeemer": { 88 | "title": "_r", 89 | "schema": { 90 | "$ref": "#/definitions/Redeemer" 91 | } 92 | }, 93 | "compiledCode": "58e4010100323232323232322533300232323232323253330083370e9000000899299980499b8748000c028dd519198008009bac300f30103010300c375400e44a66601c0022980103d87a800013232533300d3375e64600260206ea8c004c040dd50019180980099ba548008cc044dd4802a5eb804cdd2a40006602200497ae01330040040013012002301000114a22c6eb8c034c028dd50010a99980419b87480080044c8c8c92898079808001180700098051baa00216300837540026016601800460140026014004601000260086ea8004526136565734aae7555cf2ab9f5740ae855d11", 94 | "hash": "92efc746c1d47fe37bf2b9556acfa3ebb4915249ea229f282729ffdf" 95 | }, 96 | { 97 | "title": "always/always.vestingTokens.spend", 98 | "datum": { 99 | "title": "_d", 100 | "schema": { 101 | "$ref": "#/definitions/cardano~1transaction~1Datum" 102 | } 103 | }, 104 | "redeemer": { 105 | "title": "_r", 106 | "schema": { 107 | "$ref": "#/definitions/Redeemer" 108 | } 109 | }, 110 | "compiledCode": "58e4010100323232323232322533300232323232323253330083370e9000000899299980499b8748000c028dd519198008009bac300f30103010300c375400e44a66601c0022980103d87a800013232533300d3375e64600260206ea8c004c040dd50019180980099ba548008cc044dd4802a5eb804cdd2a40006602200497ae01330040040013012002301000114a22c6eb8c034c028dd50010a99980419b87480080044c8c8c92898079808001180700098051baa00216300837540026016601800460140026014004601000260086ea8004526136565734aae7555cf2ab9f5740ae855d11", 111 | "hash": "92efc746c1d47fe37bf2b9556acfa3ebb4915249ea229f282729ffdf" 112 | }, 113 | { 114 | "title": "always/always.vestingTokens.else", 115 | "compiledCode": "58e4010100323232323232322533300232323232323253330083370e9000000899299980499b8748000c028dd519198008009bac300f30103010300c375400e44a66601c0022980103d87a800013232533300d3375e64600260206ea8c004c040dd50019180980099ba548008cc044dd4802a5eb804cdd2a40006602200497ae01330040040013012002301000114a22c6eb8c034c028dd50010a99980419b87480080044c8c8c92898079808001180700098051baa00216300837540026016601800460140026014004601000260086ea8004526136565734aae7555cf2ab9f5740ae855d11", 116 | "hash": "92efc746c1d47fe37bf2b9556acfa3ebb4915249ea229f282729ffdf" 117 | } 118 | ], 119 | "definitions": { 120 | "ByteArray": { 121 | "dataType": "bytes" 122 | }, 123 | "Data": { 124 | "title": "Data", 125 | "description": "Any Plutus data." 126 | }, 127 | "DataHash": { 128 | "title": "DataHash", 129 | "dataType": "bytes" 130 | }, 131 | "Int": { 132 | "dataType": "integer" 133 | }, 134 | "Redeemer": { 135 | "title": "Redeemer", 136 | "description": "Any Plutus data." 137 | }, 138 | "cardano/transaction/Datum": { 139 | "title": "Datum", 140 | "description": "An output `Datum`.", 141 | "anyOf": [ 142 | { 143 | "title": "NoDatum", 144 | "dataType": "constructor", 145 | "index": 0, 146 | "fields": [] 147 | }, 148 | { 149 | "title": "DatumHash", 150 | "description": "A datum referenced by its hash digest.", 151 | "dataType": "constructor", 152 | "index": 1, 153 | "fields": [ 154 | { 155 | "$ref": "#/definitions/DataHash" 156 | } 157 | ] 158 | }, 159 | { 160 | "title": "InlineDatum", 161 | "description": "A datum completely inlined in the output.", 162 | "dataType": "constructor", 163 | "index": 2, 164 | "fields": [ 165 | { 166 | "$ref": "#/definitions/Data" 167 | } 168 | ] 169 | } 170 | ] 171 | }, 172 | "course/types/MarketAction": { 173 | "title": "MarketAction", 174 | "anyOf": [ 175 | { 176 | "title": "MBuy", 177 | "dataType": "constructor", 178 | "index": 0, 179 | "fields": [] 180 | }, 181 | { 182 | "title": "MEdit", 183 | "dataType": "constructor", 184 | "index": 1, 185 | "fields": [ 186 | { 187 | "title": "price", 188 | "$ref": "#/definitions/Int" 189 | } 190 | ] 191 | }, 192 | { 193 | "title": "MDelist", 194 | "dataType": "constructor", 195 | "index": 2, 196 | "fields": [] 197 | } 198 | ] 199 | }, 200 | "course/types/MarketDatum": { 201 | "title": "MarketDatum", 202 | "anyOf": [ 203 | { 204 | "title": "MarketDatum", 205 | "dataType": "constructor", 206 | "index": 0, 207 | "fields": [ 208 | { 209 | "title": "price", 210 | "$ref": "#/definitions/Int" 211 | }, 212 | { 213 | "title": "seller", 214 | "$ref": "#/definitions/ByteArray" 215 | } 216 | ] 217 | } 218 | ] 219 | } 220 | } 221 | } --------------------------------------------------------------------------------