Skip to content

Instantly share code, notes, and snippets.

@matiwinnetou
Last active January 18, 2023 07:44
Show Gist options
  • Save matiwinnetou/c84221c31e0ff850cb7de7e6e444cfc1 to your computer and use it in GitHub Desktop.
Save matiwinnetou/c84221c31e0ff850cb7de7e6e444cfc1 to your computer and use it in GitHub Desktop.
Aiken English Auction
use aiken/hash.{Blake2b_224, Hash}
use aiken/interval.{Finite, Interval, IntervalBound, PositiveInfinity}
use aiken/list
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>,
output_reference: OutputReference,
) -> Option<ValidatorHash> {
inputs
|> list.find(fn(input) { input.output_reference == output_reference })
|> 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
}
}
test must_start_after_test_works() {
let now: Int = 2
let validity_range =
Interval {
lower_bound: IntervalBound(Finite(now), True),
upper_bound: IntervalBound(PositiveInfinity, True),
}
let lower_bound = 1
must_start_after(validity_range, lower_bound)
}
test must_start_after_test_fails() {
let now: Int = 1
let validity_range =
Interval {
lower_bound: IntervalBound(Finite(now), True),
upper_bound: IntervalBound(PositiveInfinity, True),
}
let lower_bound = 2
must_start_after(validity_range, lower_bound) == False
}
test must_start_before_test_works() {
let now: Int = 1
let validity_range =
Interval {
lower_bound: IntervalBound(Finite(now), True),
upper_bound: IntervalBound(PositiveInfinity, True),
}
let lower_bound = 2
must_start_before(validity_range, lower_bound)
}
test must_start_before_test_fails() {
let now: Int = 2
let validity_range =
Interval {
lower_bound: IntervalBound(Finite(now), True),
upper_bound: IntervalBound(PositiveInfinity, True),
}
let lower_bound = 1
must_start_before(validity_range, lower_bound) == False
}
fn any_output_matches(
outputs: List<Output>,
predicate: fn(ByteArray, Value, Data) -> Bool,
) -> Bool {
list.any(
outputs,
fn(output) {
let payment_hash = get_payment_addr_hash(output.address)
predicate(payment_hash, output.value, output.datum)
},
)
}
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 datum_as_data: Data =
Datum {
seller: seller_hash_addr,
min_bid: 1,
deadline: 1673966461,
for_sale: nft,
highest_bid: 0,
highest_bidder: seller_hash_addr,
}
datum_as_data
}
test any_output_matches_test() {
let b1: ByteArray = #[1]
let b2: ByteArray = #[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 outputs: List<Output> = [
Output { address: a1, value: v1, datum: d1, reference_script: None },
Output { address: a2, value: v2, datum: d1, reference_script: None },
]
let b1 =
any_output_matches(
outputs,
fn(payment_hash, _output_value, _output_datum) { payment_hash == b1 },
)
let b2 =
any_output_matches(
outputs,
fn(_payment_hash, output_value, _output_datum) { output_value == v1 },
)
let b3 =
any_output_matches(
outputs,
fn(payment_hash, _output_value, output_datum) { output_datum == d1 },
)
b1 && b2 && b3
}
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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment