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

Type-checker ought to forbid let-bindings as last expression. #283

Closed
1 task done
Tracked by #191
matiwinnetou opened this issue Jan 17, 2023 · 2 comments · Fixed by #299
Closed
1 task done
Tracked by #191

Type-checker ought to forbid let-bindings as last expression. #283

matiwinnetou opened this issue Jan 17, 2023 · 2 comments · Fixed by #299
Assignees
Labels
typechecking Types and inference
Milestone

Comments

@matiwinnetou
Copy link

What Git revision are you using?

main as of time of writing

What operating system are you using, and which version?

  • macOS

Describe what the problem is?

The following contract crashes to compile:

use aiken/hash.{Blake2b_224, Hash}
use aiken/interval.{Finite}
use aiken/list.{any}
use aiken/option
use aiken/transaction.{
  Input, Output, OutputReference, ScriptContext, ScriptPurpose, Spend,
  ValidityRange,
}
use aiken/transaction/credential.{
  Address, Inline, PublicKeyCredential, Script, ScriptCredential,
  VerificationKey,
}
use aiken/transaction/value.{Value}

type VerificationKeyHash =
  Hash<Blake2b_224, VerificationKey>

type POSIXTime =
  Int

type Lovelace =
  Int

type ValidatorHash =
  Hash<Blake2b_224, Script>

type Datum {
  seller: VerificationKeyHash,
  min_bid: Lovelace,
  deadline: POSIXTime,
  // the asset that is being auctioned (NFT + ADA lovelace)
  for_sale: Value,
  // initialized at 0, which signifies the auction doesn't yet have valid bids
  highest_bid: Lovelace,
  highest_bidder: VerificationKeyHash,
}

type Redeemer {
  Close
  Bid { bidder: VerificationKeyHash, bid: Lovelace }
}

fn spend(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool {
  english_auction(
    datum,
    redeemer,
    ctx.transaction.validity_range,
    ctx.purpose,
    ctx.transaction.inputs,
    ctx.transaction.outputs,
  )
}

fn english_auction(
  datum: Datum,
  redeemer: Redeemer,
  validity_range: ValidityRange,
  script_purpose: ScriptPurpose,
  trx_inputs: List<Input>,
  trx_outputs: List<Output>,
) -> Bool {
  assert Spend(output_reference) = script_purpose
  assert Some(validator_hash) =
    our_validator_script_address_hash(trx_inputs, output_reference)

  when redeemer is {
    Bid(bidder, bid) ->
      bid_action(
        validator_hash,
        validity_range,
        datum,
        bidder,
        bid,
        trx_outputs,
      )
    Close ->
      close_action(
        datum: datum,
        validity_range: validity_range,
        outputs: trx_outputs,
      )
  }
}

fn bid_action(
  validator_hash: ValidatorHash,
  validity_range: ValidityRange,
  datum: Datum,
  bidder: VerificationKeyHash,
  bid: Lovelace,
  outputs: List<Output>,
) -> Bool {
  // lets see if min bid threshold is met or not
  let min_bid_threshold_not_met = bid < datum.min_bid
  // if bid is or isn't high enough
  let bid_not_high_enough = bid <= datum.highest_bid

  // delegate further
  let bid_action2 =
    bid_action2(
      datum: datum,
      bid: bid,
      bidder: bidder,
      validity_range: validity_range,
      validator_hash: validator_hash,
      outputs: outputs,
    )

  min_bid_threshold_not_met && bid_not_high_enough && bid_action2
}

fn bid_action2(
  datum: Datum,
  bid: Lovelace,
  bidder: VerificationKeyHash,
  validity_range: ValidityRange,
  validator_hash: ValidatorHash,
  outputs: List<Output>,
) -> Bool {
  let new_expected_datum: Datum =
    Datum { ..datum, highest_bid: bid, highest_bidder: bidder }

  // it makes sense to approve biding action on this contract only if deadline has not been reached yet
  let can_still_bid = must_start_before(validity_range, datum.deadline)

  let has_new_bid_have_token_and_new_bid =
    has_new_bid_have_token_and_new_bid(
      validator_hash,
      bid,
      new_expected_datum,
      outputs,
    )
  // we need to check if current bidder has been repaid
  let is_old_bidder_repaid = is_old_bidder_repaid(datum, outputs)

  can_still_bid && has_new_bid_have_token_and_new_bid && is_old_bidder_repaid
}

fn close_action(
  datum: Datum,
  validity_range: ValidityRange,
  outputs: List<Output>,
) -> Bool {
  let is_min_bid_threshold_not_met = datum.highest_bid < datum.min_bid

  if is_min_bid_threshold_not_met {
    close_action_winner_gets_token(
      datum: datum,
      validity_range: validity_range,
      outputs: outputs,
    )
  } else {
    close_action_seller_repaid(
      datum: datum,
      validity_range: validity_range,
      outputs: outputs,
    )
  }
}

fn close_action_seller_repaid(
  datum: Datum,
  validity_range: ValidityRange,
  outputs: List<Output>,
) -> Bool {
  // we need to check if people placing bids have managed to place bet as high as minimum bid
  let is_min_bid_threshold_not_met = datum.highest_bid < datum.min_bid

  // if treshold is not met then we need to check number of things...
  if is_min_bid_threshold_not_met {
    // for starters if bid item has been returned to the seller by the off chain code for close action to be allowed
    let is_for_sale_item_returned =
      is_for_sale_returned_to_the_seller(datum, outputs)
    // is bidding on item no longer possible then close action can be completed / allowed
    // if bidding it is still possible, we should NOT allow owner to claim NFT while auction is running
    let is_bidding_no_longer_possible =
      must_start_after(validity_range, datum.deadline)

    is_bidding_no_longer_possible && is_for_sale_item_returned
  } else {
    False
  }
}

fn close_action_winner_gets_token(
  datum: Datum,
  validity_range: ValidityRange,
  outputs: List<Output>,
) -> Bool {
  // lets check if bidding is no longer possible (deadline passed)
  let is_bidding_no_longer_possible =
    must_start_after(validity_range, datum.deadline)

  // seller received highest bid in lovelaces
  let seller_receives_highest_bid =
    seller_receives_highest_bid(
      datum: datum,
      highest_bid: datum.highest_bid,
      outputs: outputs,
    )

  // eventually of course winner got his / her token and we need to assert for this
  let winner_receives_for_sale_token =
    winner_gets_auctioned_token(datum: datum, outputs: outputs)

  is_bidding_no_longer_possible && seller_receives_highest_bid && winner_receives_for_sale_token
}

// we need to check that seller received the highest bid
fn seller_receives_highest_bid(
  datum: Datum,
  highest_bid: Lovelace,
  outputs: List<Output>,
) -> Bool {
  outputs
  |> any_output_matches(
  fn(payment_hash, output_value, _output_datum) {
    payment_hash == datum.seller && output_value == value.from_lovelace(
      highest_bid,
    )
  })
  
}

// this function will verify if old bidder got repaid
fn is_old_bidder_repaid(datum: Datum, outputs: List<Output>) -> Bool {
  outputs
  |> any_output_matches(
  fn(payment_hash, output_value, _output_datum) {
    // since we have old datum from the previous transaction it contains current highest bidder and when we spend this UTxO, assuming new bid is successful, new datum will include new highest_bidder
    let current_highest_bid = datum.highest_bid
    let current_highest_bidder_hash = datum.highest_bidder
    payment_hash == current_highest_bidder_hash && output_value == value.from_lovelace(
      current_highest_bid,
    )
  })
  
}

// on bid action - new bidder should have new token including bid amount (lovelaces) in one of the UTxO outputs
fn has_new_bid_have_token_and_new_bid(
  validator_hash: ValidatorHash,
  bid: Lovelace,
  new_expected_datum: Datum,
  outputs: List<Output>,
) -> Bool {
  let datum_as_data: Data = new_expected_datum

  outputs
  |> any_output_matches(
  fn(payment_hash, output_value, output_datum) {
    // we need to verify if new bid also contains datum and if datum data matches
    // it needs to have exactly the same values as defined by new_expected_datum requirements
    let new_bid_contains_datum = output_datum == datum_as_data

    new_bid_contains_datum && payment_hash == validator_hash && output_value == value.add(
      new_expected_datum.for_sale,
      value.from_lovelace(bid),
    )
  })
  
}

// we need to check that after auction has finished if winner received auctioned token / NFT
fn winner_gets_auctioned_token(datum: Datum, outputs: List<Output>) -> Bool {
  outputs
  |> any_output_matches(
  fn(payment_hash, value, _datum) {
    payment_hash == datum.seller && value == value.without_lovelace(
      datum.for_sale,
    )
  })
  
}

// while closing auction we need to check if off-chain code returned the item to the seller
fn is_for_sale_returned_to_the_seller(
  datum: Datum,
  outputs: List<Output>,
) -> Bool {
  outputs
  |> any_output_matches(
  fn(payment_hash, output_value, _output_datum) {
    payment_hash == datum.seller && output_value == datum.for_sale
  })
  
}

fn our_validator_script_address_hash(
  inputs: List<Input>,
  or: OutputReference,
) -> Option<ValidatorHash> {
  let maybe_input: Option<Input> =
    list.find(inputs, fn(input) { input.output_reference == or })

  maybe_input
  |> option.map(fn(v) { v.output })
  |> option.map(fn(v) { v.address })
  |> option.map(fn(v) { v.payment_credential })
  |> option.map(
  fn(v) {
    when v is {
      ScriptCredential(hash) -> Some(hash)
      _ -> None
    }
  })
  
  |> option.flatten()
}

fn must_start_before(range: ValidityRange, lower_bound: POSIXTime) -> Bool {
  when range.lower_bound.bound_type is {
    Finite(now) -> now < lower_bound
    _ -> False
  }
}

fn must_start_after(range: ValidityRange, lower_bound: POSIXTime) -> Bool {
  when range.lower_bound.bound_type is {
    Finite(now) -> now > lower_bound
    _ -> False
  }
}

fn any_output_matches(
  outputs: List<Output>,
  predicate: fn(ByteArray, Value, Data) -> Bool,
) -> Bool {
  any(
    outputs,
    fn(output) {
      let payment_hash = get_payment_addr_hash(output.address)
      predicate(payment_hash, output.value, output.datum)
    },
  )
}

// pub type Output {
//   address: Address,
//   value: Value,
//   datum: Datum,
//   reference_script: Option<Hash<Blake2b_224, Script>>,
// }

// type Datum {
//   seller: VerificationKeyHash,
//   min_bid: Lovelace,
//   deadline: POSIXTime,
//   // the asset that is being auctioned (NFT + ADA lovelace)
//   for_sale: Value,
//   // initialized at 0, which signifies the auction doesn't yet have valid bids
//   highest_bid: Lovelace,
//   highest_bidder: VerificationKeyHash,
// }

fn mock_datum_data() -> Data {
  let seller_hash_addr = #[1]
  let policy_id1 = #[2]
  let asset_name1 = #[3]

  let nft = value.from_asset(policy_id1, asset_name1, 1)

  let d1: Data =
    Datum {
      seller: seller_hash_addr,
      min_bid: 1,
      deadline: 1673966461,
      for_sale: nft,
      highest_bid: 0,
      highest_bidder: seller_hash_addr,
    }
}

test any_output_matches1() {
  let b1 = #[1]
  let b2 = #[2]

  let v1 = value.from_lovelace(1)
  let v2 = value.from_lovelace(2)

  let d1 = mock_datum_data()

  let a1 =
    Address {
      payment_credential: PublicKeyCredential(b1),
      stake_credential: None,
    }

  let a2 =
    Address {
      payment_credential: PublicKeyCredential(b2),
      stake_credential: None,
    }

  let b1 =
    Address {
      payment_credential: PublicKeyCredential(b1),
      stake_credential: None,
    }

  let outputs: List<Output> = [
    Output { address: a1, value: v1, datum: d1, reference_script: None },
    Output { address: a2, value: v2, datum: d1, reference_script: None },
  ]

  any_output_matches(outputs, fn(_payment_hash, _value, _datum) { True })
}

fn get_payment_addr_hash(address: Address) -> VerificationKeyHash {
  when address.payment_credential is {
    PublicKeyCredential(hash) -> hash
    ScriptCredential(hash) -> hash
  }
}

test get_payment_addr_hash_public_key() {
  let b1 = #[1]
  let b2 = #[2]

  let addr =
    Address {
      payment_credential: PublicKeyCredential(b1),
      stake_credential: Some(Inline(ScriptCredential(b2))),
    }

  get_payment_addr_hash(addr) == b1
}

test get_payment_addr_hash_script_key() {
  let b2 = #[2]
  let b3 = #[3]

  let addr =
    Address {
      payment_credential: ScriptCredential(b3),
      stake_credential: Some(Inline(ScriptCredential(b2))),
    }

  get_payment_addr_hash(addr) == b3
}
mati@Mateuszs-MacBook-Pro english_auction % aiken check
    Compiling aiken-lang/stdlib a972f8e4d89d0998911423d7d6a1b99dd94dd1b9 (/Users/mati/Devel/OpenSource/aiken/examples/english_auction/build/packages/aiken-lang-stdlib)
    Compiling aiken-lang/english_auction 0.0.0 (/Users/mati/Devel/OpenSource/aiken/examples/english_auction)
Error: 
  × Main thread panicked.
  ├─▶ at crates/aiken-lang/src/uplc.rs:3435:32
  ╰─▶ called `Option::unwrap()` on a `None` value
  help: set the `RUST_BACKTRACE=1` environment variable to display a backtrace.

What should be the expected behavior?

It should not crash.

@matiwinnetou
Copy link
Author

Actually it turns out this code is the problem:

fn mock_datum_data() -> Data {
  let seller_hash_addr = #[1]
  let policy_id1 = #[2]
  let asset_name1 = #[3]

  let nft = value.from_asset(policy_id1, asset_name1, 1)

  let d1: Data =
    Datum {
      seller: seller_hash_addr,
      min_bid: 1,
      deadline: 1673966461,
      for_sale: nft,
      highest_bid: 0,
      highest_bidder: seller_hash_addr,
    }
}

there is an obvious error there... d1 should be actually returned but instead nothing was returned.

@KtorZ KtorZ self-assigned this Jan 17, 2023
@KtorZ KtorZ changed the title Aiken fails to compile and crashes Type-checker ought to forbid let-bindings as last expression. Jan 17, 2023
@KtorZ KtorZ added the typechecking Types and inference label Jan 17, 2023
@KtorZ KtorZ added this to the Language PoC milestone Jan 17, 2023
@KtorZ KtorZ mentioned this issue Jan 17, 2023
27 tasks
@rvcas
Copy link
Member

rvcas commented Jan 17, 2023

Yea no problem, we already wanted to do this eventually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typechecking Types and inference
Projects
Status: 🚀 Released
Development

Successfully merging a pull request may close this issue.

3 participants