From 1d6434179b5c27808b03410bda76c6fc28a82080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Wed, 19 Apr 2023 17:01:43 +0200 Subject: [PATCH 1/3] RedeemStrategy --- .../sources/config/redeem/pseudorandom.move | 5 +- .../sources/config/redeem/strategy.move | 146 ++++++++++++ launchpad_v2/sources/inventory/factory.move | 8 +- launchpad_v2/sources/inventory/warehouse.move | 223 ++++++++++++++---- launchpad_v2/sources/venue.move | 31 ++- sources/launchpad/inventory.move | 3 +- sources/launchpad/listing.move | 3 +- sources/launchpad/redeem_random.move | 114 +++++++++ sources/launchpad/warehouse.move | 98 +------- 9 files changed, 457 insertions(+), 174 deletions(-) create mode 100644 launchpad_v2/sources/config/redeem/strategy.move create mode 100644 sources/launchpad/redeem_random.move diff --git a/launchpad_v2/sources/config/redeem/pseudorandom.move b/launchpad_v2/sources/config/redeem/pseudorandom.move index 05e52192..08645ba9 100644 --- a/launchpad_v2/sources/config/redeem/pseudorandom.move +++ b/launchpad_v2/sources/config/redeem/pseudorandom.move @@ -6,6 +6,7 @@ module launchpad_v2::pseudorand_redeem { use sui::dynamic_field as df; use launchpad_v2::launchpad::LaunchCap; + use launchpad_v2::redeem_strategy; use launchpad_v2::venue::{Self, Venue, RedeemReceipt, NftCert}; use originmate::pseudorandom; @@ -97,7 +98,6 @@ module launchpad_v2::pseudorand_redeem { let contract_commitment = pseudorandom::rand_no_counter(nonce, ctx); let inv_index = select(rand_redeem.kiosk_precision, &contract_commitment); - let nft_rel_index = select(rand_redeem.nft_precision, &contract_commitment); let (inv_id, inv_type) = get_inventory_data(venue, inv_index); @@ -106,8 +106,7 @@ module launchpad_v2::pseudorand_redeem { venue, inv_type, inv_id, - rand_redeem.nft_precision, - nft_rel_index, + redeem_strategy::pseudorandom(), ctx, ) } diff --git a/launchpad_v2/sources/config/redeem/strategy.move b/launchpad_v2/sources/config/redeem/strategy.move new file mode 100644 index 00000000..4d52b1fe --- /dev/null +++ b/launchpad_v2/sources/config/redeem/strategy.move @@ -0,0 +1,146 @@ +module launchpad_v2::redeem_strategy { + use sui::object::{ID, UID}; + use sui::dynamic_field as df; + + use nft_protocol::redeem_random::RedeemCommitment; + + /// Could not register redeem parameters as they already exist + const EConflictingParameters: u64 = 1; + + /// Could not extract redeem parameters as they were not registered on the + /// object + const EInvalidParameters: u64 = 2; + + struct RedeemStrategy has store, copy, drop { + /// Redeem strategy flag + /// + /// Valid values: + /// + /// 0 - Sequential + /// + /// 1 - Pseudorandom + /// + /// 2 - Random + /// + /// 3 - ByIndex + /// + /// 4 - ByID + /// + /// Note that random, index, and ID strategies will registed + flag: u8, + } + + public fun new(flag: u8): RedeemStrategy { + RedeemStrategy { flag } + } + + /// Sequential redeem strategy + public fun sequential(): RedeemStrategy { + new(0) + } + + /// Pseudorandom redeem strategy + public fun pseudorandom(): RedeemStrategy { + new(1) + } + + /// Random redeem strategy + public fun random(): RedeemStrategy { + new(2) + } + + /// Redeem strategy by NFT index + public fun by_index(): RedeemStrategy { + new(3) + } + + /// Redeem strategy by NFT ID + public fun by_id(): RedeemStrategy { + new(4) + } + + /// Return raw `RedeemStrategy` flag + public fun flag(strategy: &RedeemStrategy): &u8 { + &strategy.flag + } + + /// Return whether strategy is sequential + public fun is_sequential(strategy: &RedeemStrategy): bool { + strategy.flag == 0 + } + + /// Return whether strategy is pseudorandom + public fun is_pseudorandom(strategy: &RedeemStrategy): bool { + strategy.flag == 1 + } + + /// Return whether strategy is random + public fun is_random(strategy: &RedeemStrategy): bool { + strategy.flag == 2 + } + + /// Return whether strategy is by index + public fun is_by_index(strategy: &RedeemStrategy): bool { + strategy.flag == 3 + } + + /// Return whether strategy is by ID + public fun is_by_id(strategy: &RedeemStrategy): bool { + strategy.flag == 4 + } + + struct ParametersKey has copy, drop, store {} + + public fun register_parameters_random( + object: &mut UID, + commitment: RedeemCommitment, + ) { + add_parameters(object, commitment) + } + + public fun register_parameters_by_index(object: &mut UID, index: u64) { + add_parameters(object, index) + } + + public fun register_parameters_by_id(object: &mut UID, id: ID) { + add_parameters(object, id) + } + + public fun extract_parameters_random(object: &mut UID): RedeemCommitment { + remove_parameters(object) + } + + public fun extract_parameters_by_index(object: &mut UID): u64 { + remove_parameters(object) + } + + public fun extract_parameters_by_id(object: &mut UID): ID { + remove_parameters(object) + } + + // === Helpers === + + public fun add_parameters(object: &mut UID, parameters: T) { + assert_no_parameters(object); + df::add(object, ParametersKey {}, parameters) + } + + public fun remove_parameters(object: &mut UID, ): T { + assert_parameters(object); + df::remove(object, ParametersKey {}) + } + + public fun has_parameters(object: &UID): bool { + df::exists_with_type( + object, ParametersKey {}, + ) + } + + public fun assert_parameters(object: &UID) { + assert!(has_parameters(object), EInvalidParameters); + } + + public fun assert_no_parameters(object: &UID) { + assert!(!has_parameters(object), EConflictingParameters); + } +} diff --git a/launchpad_v2/sources/inventory/factory.move b/launchpad_v2/sources/inventory/factory.move index c3221808..56f927bf 100644 --- a/launchpad_v2/sources/inventory/factory.move +++ b/launchpad_v2/sources/inventory/factory.move @@ -4,7 +4,6 @@ module launchpad_v2::factory { use sui::object::{Self, UID}; use sui::tx_context::TxContext; - use sui::math; use sui::bcs::{Self, BCS}; use nft_protocol::mint_pass::{Self, MintPass}; @@ -72,14 +71,9 @@ module launchpad_v2::factory { venue::assert_cert_buyer(&certificate, ctx); venue::assert_cert_inventory(&certificate, object::id(factory)); - let index = math::divide_and_round_up( - factory.total_deposited * venue::cert_relative_index(&certificate), - venue::cert_index_scale(&certificate) - ); - venue::consume_certificate(Witness {}, factory, certificate); - redeem_mint_pass_at_index(factory, index, ctx) + redeem_mint_pass_at_index(factory, 0, ctx) } /// Redeems NFT from specific index in `Warehouse` diff --git a/launchpad_v2/sources/inventory/warehouse.move b/launchpad_v2/sources/inventory/warehouse.move index b775a39f..1eabda6c 100644 --- a/launchpad_v2/sources/inventory/warehouse.move +++ b/launchpad_v2/sources/inventory/warehouse.move @@ -12,45 +12,43 @@ module launchpad_v2::warehouse { use std::vector; use sui::transfer; - use sui::math; use sui::dynamic_object_field as dof; use sui::tx_context::{Self, TxContext}; use sui::object::{Self, ID , UID}; + use nft_protocol::redeem_random::{Self, RedeemCommitment}; + use launchpad_v2::venue::{Self, NftCert}; + use launchpad_v2::redeem_strategy; use originmate::pseudorandom; /// `Warehouse` does not have NFTs left to withdraw /// - /// Call `Warehouse::deposit_nft` or `Listing::add_nft` to add NFTs. - const EEMPTY: u64 = 1; + /// Call `warehouse::deposit_nft` or `listing::add_nft` to add NFTs. + const EEmpty: u64 = 1; /// `Warehouse` still has NFTs left to withdraw /// - /// Call `Warehouse::redeem_nft` or a `Listing` market to withdraw remaining + /// Call `warehouse::redeem_nft` or a `Listing` market to withdraw remaining /// NFTs. - const ENOT_EMPTY: u64 = 2; + const ENotEmpty: u64 = 2; /// `Warehouse` does not have NFT at specified index /// - /// Call `Warehouse::redeem_nft_at_index` with an index that exists. - const EINDEX_OUT_OF_BOUNDS: u64 = 3; - - /// Attempted to construct a `RedeemCommitment` with a hash length - /// different than 32 bytes - const EINVALID_COMMITMENT_LENGTH: u64 = 4; + /// Call `warehouse::redeem_nft_at_index` with an index that exists. + const EIndexOutOfBounds: u64 = 3; - /// Commitment in `RedeemCommitment` did not match original value committed + /// `Warehouse` did not contain NFT object with given ID /// - /// Call `Warehosue::random_redeem_nft` with the correct commitment. - const EINVALID_COMMITMENT: u64 = 5; + /// Call `warehouse::redeem_nft_with_id` with an ID that exists. + const EInvalidNft: u64 = 4; + const EUnsupportedRedeemStrategy: u64 = 5; struct Witness has drop {} - - /// `Warehouse` object which stores NFTs + /// `Warehouse` object which stores NFTs of type `T` /// /// The reason that the type is limited is to easily support random /// withdrawals. If multiple types are allowed then user will not be able @@ -75,7 +73,7 @@ module launchpad_v2::warehouse { } /// Creates a `Warehouse` and transfers to transaction sender - public entry fun init_warehouse(ctx: &mut TxContext) { + public entry fun init_warehouse(ctx: &mut TxContext) { transfer::public_transfer(new(ctx), tx_context::sender(ctx)); } @@ -94,17 +92,6 @@ module launchpad_v2::warehouse { dof::add(&mut warehouse.id, nft_id, nft); } - /// Redeems NFT from `Warehouse` - /// - /// Endpoint is unprotected and relies on safely obtaining a mutable - /// reference to `Warehouse`. - /// - /// `Warehouse` may not change the logical owner of an `Nft` when - /// redeeming as this would allow royalties to be trivially bypassed. - /// - /// #### Panics - /// - /// Panics if `Warehouse` is empty. public fun redeem_nft( warehouse: &mut Warehouse, certificate: NftCert, @@ -114,37 +101,66 @@ module launchpad_v2::warehouse { venue::assert_cert_buyer(&certificate, ctx); venue::assert_cert_inventory(&certificate, object::id(warehouse)); - // - let index = math::divide_and_round_up( - warehouse.total_deposited * venue::cert_relative_index(&certificate), - venue::cert_index_scale(&certificate) - ); + let strategy = &venue::cert_redeem_strategy(&certificate); + + let nft = if (redeem_strategy::is_sequential(strategy)) { + redeem_nft_sequential(warehouse) + } else if (redeem_strategy::is_pseudorandom(strategy)) { + redeem_pseudorandom_nft(warehouse, ctx) + } else if (redeem_strategy::is_random(strategy)) { + let commitment = redeem_strategy::extract_parameters_random( + venue::cert_uid_mut(&mut certificate), + ); + // TODO: Figure out best way to pass user commitment + // for now its `vector::empty()` + redeem_random_nft(warehouse, commitment, vector::empty(), ctx) + } else if (redeem_strategy::is_by_index(strategy)) { + let index = redeem_strategy::extract_parameters_by_index( + venue::cert_uid_mut(&mut certificate), + ); + + redeem_nft_at_index(warehouse, index) + } else if (redeem_strategy::is_by_id(strategy)) { + let id = redeem_strategy::extract_parameters_by_id( + venue::cert_uid_mut(&mut certificate), + ); + + redeem_nft_with_id(warehouse, id) + } else { + abort(EUnsupportedRedeemStrategy) + }; venue::consume_certificate(Witness {}, warehouse, certificate); + nft + } - redeem_nft_at_index(warehouse, index) + public entry fun redeem_nft_and_transfer( + warehouse: &mut Warehouse, + certificate: NftCert, + ctx: &mut TxContext, + ) { + let nft: T = redeem_nft(warehouse, certificate, ctx); + transfer::public_transfer(nft, tx_context::sender(ctx)); } - /// Redeems NFT from `Warehouse` and transfers to sender + /// Redeems NFT from `Warehouse` sequentially /// - /// See `redeem_nft` for more details. - /// - /// #### Usage + /// Endpoint is unprotected and relies on safely obtaining a mutable + /// reference to `Warehouse`. /// - /// Entry mint functions like `suimarines::mint_nft` take an `Warehouse` - /// object to deposit into. Calling `redeem_nft_and_transfer` allows one to - /// withdraw an NFT and own it directly. + /// `Warehouse` may not change the logical owner of an `Nft` when + /// redeeming as this would allow royalties to be trivially bypassed. /// /// #### Panics /// /// Panics if `Warehouse` is empty. - public entry fun redeem_nft_and_transfer( + fun redeem_nft_sequential( warehouse: &mut Warehouse, - certificate: NftCert, - ctx: &mut TxContext, - ) { - let nft = redeem_nft(warehouse, certificate, ctx); - transfer::public_transfer(nft, tx_context::sender(ctx)); + ): T { + let nfts = &mut warehouse.nfts; + assert!(!vector::is_empty(nfts), EEmpty); + + dof::remove(&mut warehouse.id, vector::pop_back(nfts)) } /// Redeems NFT from specific index in `Warehouse` @@ -166,7 +182,7 @@ module launchpad_v2::warehouse { ): T { let nfts = &mut warehouse.nfts; let length = vector::length(nfts); - assert!(index < vector::length(nfts), EINDEX_OUT_OF_BOUNDS); + assert!(index < vector::length(nfts), EIndexOutOfBounds); let nft_id = *vector::borrow(nfts, index); @@ -180,19 +196,124 @@ module launchpad_v2::warehouse { dof::remove(&mut warehouse.id, nft_id) } + /// Redeems NFT with specific ID from `Warehouse` + /// + /// Does not retain original order of NFTs in the bookkeeping vector. + /// + /// Endpoint is unprotected and relies on safely obtaining a mutable + /// reference to `Warehouse`. + /// + /// `Warehouse` may not change the logical owner of an `Nft` when + /// redeeming as this would allow royalties to be trivially bypassed. + /// + /// #### Panics + /// + /// Panics if NFT with ID does not exist in `Warehouse`. + fun redeem_nft_with_id( + warehouse: &mut Warehouse, + nft_id: ID, + ): T { + let nfts = &mut warehouse.nfts; + let supply = vector::length(nfts); + + let idx = 0; + while (idx < supply) { + let t_nft_id = vector::borrow(nfts, idx); + + if (&nft_id == t_nft_id) { + return redeem_nft_at_index(warehouse, idx) + }; + + idx = idx + 1; + }; + + assert!(false, EInvalidNft); + // Provide correct return type signature but will fail eitherway + redeem_nft_at_index(warehouse, idx) + } + + /// Pseudo-randomly redeems NFT from `Warehouse` + /// + /// Endpoint is susceptible to validator prediction of the resulting index, + /// use `random_redeem_nft` instead. + /// + /// Endpoint is unprotected and relies on safely obtaining a mutable + /// reference to `Warehouse`. + /// + /// `Warehouse` may not change the logical owner of an `Nft` when + /// redeeming as this would allow royalties to be trivially bypassed. + /// + /// #### Panics + /// + /// Panics if `Warehouse` is empty + fun redeem_pseudorandom_nft( + warehouse: &mut Warehouse, + ctx: &mut TxContext, + ): T { + let supply = supply(warehouse); + assert!(supply != 0, EEmpty); + + // Use supply of `Warehouse` as an additional nonce factor + let nonce = vector::empty(); + vector::append(&mut nonce, sui::bcs::to_bytes(&supply)); + + let contract_commitment = pseudorandom::rand_no_counter(nonce, ctx); + + let index = select(supply, &contract_commitment); + redeem_nft_at_index(warehouse, index) + } + + /// Randomly redeems NFT from `Warehouse` + /// + /// Requires a `RedeemCommitment` created by the user in a separate + /// transaction to ensure that validators may not bias results favorably. + /// You can obtain a `RedeemCommitment` by calling + /// `init_redeem_commitment`. + /// + /// Endpoint is unprotected and relies on safely obtaining a mutable + /// reference to `Warehouse`. + /// + /// `Warehouse` may not change the logical owner of an `Nft` when + /// redeeming as this would allow royalties to be trivially bypassed. + /// + /// #### Panics + /// + /// Panics if `Warehouse` is empty or `user_commitment` does not match the + /// hashed commitment in `RedeemCommitment`. + fun redeem_random_nft( + warehouse: &mut Warehouse, + commitment: RedeemCommitment, + user_commitment: vector, + ctx: &mut TxContext, + ): T { + let (_, contract_commitment) = + redeem_random::consume_commitment(commitment, user_commitment); + + // Construct randomized index + let supply = supply(warehouse); + assert!(supply != 0, EEmpty); + + vector::append(&mut user_commitment, contract_commitment); + // Use supply of `Warehouse` as a additional nonce factor + vector::append(&mut user_commitment, sui::bcs::to_bytes(&supply)); + + let contract_commitment = pseudorandom::rand_no_counter(user_commitment, ctx); + + let index = select(supply, &contract_commitment); + redeem_nft_at_index(warehouse, index) + } /// Destroys `Warehouse` /// /// #### Panics /// /// Panics if `Warehouse` is not empty - public entry fun destroy(warehouse: Warehouse) { + public entry fun destroy(warehouse: Warehouse) { assert_is_empty(&warehouse); let Warehouse { id, nfts: _, total_deposited: _ } = warehouse; object::delete(id); } - // === Getter Functions === /// Return how many `Nft` there are to sell @@ -224,7 +345,7 @@ module launchpad_v2::warehouse { /// Asserts that `Warehouse` is empty public fun assert_is_empty(warehouse: &Warehouse) { - assert!(is_empty(warehouse), ENOT_EMPTY); + assert!(is_empty(warehouse), ENotEmpty); } // === Utils === diff --git a/launchpad_v2/sources/venue.move b/launchpad_v2/sources/venue.move index d0acfe2e..e00c0c8a 100644 --- a/launchpad_v2/sources/venue.move +++ b/launchpad_v2/sources/venue.move @@ -14,6 +14,8 @@ module launchpad_v2::venue { use nft_protocol::witness; use nft_protocol::request::{Policy, PolicyCap}; use nft_protocol::utils_supply::{Self, Supply}; + + use launchpad_v2::redeem_strategy::RedeemStrategy; use launchpad_v2::launchpad::{Self, LaunchCap}; use launchpad_v2::auth_request::{Self, AuthRequest, AUTH_REQUEST}; use launchpad_v2::proceeds::{Self, Proceeds}; @@ -104,9 +106,7 @@ module launchpad_v2::venue { nft_type: TypeName, buyer: address, inventory: ID, - index_scale: u64, - // Relative index of the NFT in the Warehouse - relative_index: u64, + redeem_strategy: RedeemStrategy, } /// Event signalling that `Nft` was sold by `Listing` @@ -379,8 +379,7 @@ module launchpad_v2::venue { venue: &Venue, nft_type: TypeName, inventory_id: ID, - relative_index: u64, - index_scale: u64, + redeem_strategy: RedeemStrategy, ctx: &mut TxContext, ): NftCert { assert_called_from_redeem_method(venue); @@ -391,8 +390,7 @@ module launchpad_v2::venue { nft_type, buyer: tx_context::sender(ctx), inventory: inventory_id, - index_scale, - relative_index, + redeem_strategy, } } @@ -417,8 +415,7 @@ module launchpad_v2::venue { nft_type: _, buyer: _, inventory: _, - index_scale: _, - relative_index: _, + redeem_strategy: _, } = cert; object::delete(id); @@ -473,10 +470,6 @@ module launchpad_v2::venue { // === Venue Getter Functions === - public fun get_venue_id(cert: &NftCert): ID { - cert.venue_id - } - public fun listing_id(venue: &Venue): &ID { &venue.listing_id } @@ -604,12 +597,16 @@ module launchpad_v2::venue { cert.inventory } - public fun cert_relative_index(cert: &NftCert): u64 { - cert.relative_index + public fun cert_redeem_strategy(cert: &NftCert): RedeemStrategy { + cert.redeem_strategy + } + + public fun cert_uid(cert: &NftCert): &UID { + &cert.id } - public fun cert_index_scale(cert: &NftCert): u64 { - cert.index_scale + public fun cert_uid_mut(cert: &mut NftCert): &mut UID { + &mut cert.id } // === Private Functions === diff --git a/sources/launchpad/inventory.move b/sources/launchpad/inventory.move index fb365270..7a0f3803 100644 --- a/sources/launchpad/inventory.move +++ b/sources/launchpad/inventory.move @@ -12,7 +12,8 @@ module nft_protocol::inventory { use sui::dynamic_field as df; use nft_protocol::utils::{Self, Marker}; - use nft_protocol::warehouse::{Self, Warehouse, RedeemCommitment}; + use nft_protocol::redeem_random::RedeemCommitment; + use nft_protocol::warehouse::{Self, Warehouse}; /// `Inventory` is not a `Warehouse` /// diff --git a/sources/launchpad/listing.move b/sources/launchpad/listing.move index 95be91df..355bd9d7 100644 --- a/sources/launchpad/listing.move +++ b/sources/launchpad/listing.move @@ -42,7 +42,8 @@ module nft_protocol::listing { use nft_protocol::err; use nft_protocol::witness::Witness as DelegatedWitness; use nft_protocol::inventory::{Self, Inventory}; - use nft_protocol::warehouse::{Self, Warehouse, RedeemCommitment}; + use nft_protocol::redeem_random::RedeemCommitment; + use nft_protocol::warehouse::{Self, Warehouse}; use nft_protocol::marketplace::{Self as mkt, Marketplace}; use nft_protocol::proceeds::{Self, Proceeds}; use nft_protocol::venue::{Self, Venue}; diff --git a/sources/launchpad/redeem_random.move b/sources/launchpad/redeem_random.move new file mode 100644 index 00000000..e1ac3dd9 --- /dev/null +++ b/sources/launchpad/redeem_random.move @@ -0,0 +1,114 @@ +module nft_protocol::redeem_random { + use std::vector; + + use sui::transfer; + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + + use originmate::pseudorandom; + + /// Attempted to construct a `RedeemCommitment` with a hash length + /// different than 32 bytes + const EInvalidCommitmentLength: u64 = 5; + + /// Commitment in `RedeemCommitment` did not match original value committed + /// + /// Call `warehouse::random_redeem_nft` with the correct commitment. + const EInvalidCommitment: u64 = 6; + + /// Used for the client to commit a pseudo-random + struct RedeemCommitment has key, store { + /// `RedeemCommitment` ID + id: UID, + /// Hashed sender commitment + /// + /// Sender will have to provide the pre-hashed value to be able to use + /// this `RedeemCommitment`. This value can be pseudo-random as long + /// as it is not predictable by the validator. + hashed_sender_commitment: vector, + /// Open commitment made by validator + contract_commitment: vector, + } + + /// Create a new `RedeemCommitment` + /// + /// Contract commitment must be unfeasible to predict by the transaction + /// sender. The underlying value of the commitment can be pseudo-random as + /// long as it is not predictable by the validator. + /// + /// #### Panics + /// + /// Panics if commitment is not 32 bytes. + public fun new_commitment( + hashed_sender_commitment: vector, + ctx: &mut TxContext, + ): RedeemCommitment { + assert!( + vector::length(&hashed_sender_commitment) != 32, + EInvalidCommitmentLength, + ); + + RedeemCommitment { + id: object::new(ctx), + hashed_sender_commitment, + contract_commitment: pseudorandom::rand_with_ctx(ctx), + } + } + + /// Creates a new `RedeemCommitment` and transfers it to the transaction + /// caller. + /// + /// Contract commitment must be unfeasible to predict by the transaction + /// caller. The underlying value of the commitment can be pseudo-random as + /// long as it is not predictable by the validator. + /// + /// #### Panics + /// + /// Panics if commitment is not 32 bytes. + public entry fun init_commitment( + hashed_sender_commitment: vector, + ctx: &mut TxContext, + ) { + let commitment = new_commitment(hashed_sender_commitment, ctx); + transfer::transfer(commitment, tx_context::sender(ctx)); + } + + /// Consumes `RedeemCommitment` + /// + /// #### Panics + /// + /// Panics if `user_commitment` does not match the hashed commitment in + /// `RedeemCommitment`. + public fun consume_commitment( + commitment: RedeemCommitment, + user_commitment: vector, + ): (vector, vector) { + // Verify user commitment + let RedeemCommitment { + id, + hashed_sender_commitment, + contract_commitment + } = commitment; + + object::delete(id); + + let user_commitment = std::hash::sha3_256(user_commitment); + assert!( + user_commitment == hashed_sender_commitment, + EInvalidCommitment, + ); + + (hashed_sender_commitment, contract_commitment) + } + + /// Deletes `RedeemCommitment` + public entry fun delete_commitment(commitment: RedeemCommitment) { + let RedeemCommitment { + id, + hashed_sender_commitment: _, + contract_commitment: _, + } = commitment; + + object::delete(id); + } +} diff --git a/sources/launchpad/warehouse.move b/sources/launchpad/warehouse.move index ac8c8f05..2b161528 100644 --- a/sources/launchpad/warehouse.move +++ b/sources/launchpad/warehouse.move @@ -16,6 +16,8 @@ module nft_protocol::warehouse { use sui::tx_context::{Self, TxContext}; use sui::object::{Self, ID , UID}; + use nft_protocol::redeem_random::{Self, RedeemCommitment}; + use originmate::pseudorandom; /// `Warehouse` does not have NFTs left to withdraw @@ -39,29 +41,6 @@ module nft_protocol::warehouse { /// Call `warehouse::redeem_nft_with_id` with an ID that exists. const EINVALID_NFT_ID: u64 = 4; - /// Attempted to construct a `RedeemCommitment` with a hash length - /// different than 32 bytes - const EINVALID_COMMITMENT_LENGTH: u64 = 5; - - /// Commitment in `RedeemCommitment` did not match original value committed - /// - /// Call `warehouse::random_redeem_nft` with the correct commitment. - const EINVALID_COMMITMENT: u64 = 6; - - /// Used for the client to commit a pseudo-random - struct RedeemCommitment has key { - /// `RedeemCommitment` ID - id: UID, - /// Hashed sender commitment - /// - /// Sender will have to provide the pre-hashed value to be able to use - /// this `RedeemCommitment`. This value can be pseudo-random as long - /// as it is not predictable by the validator. - hashed_sender_commitment: vector, - /// Open commitment made by validator - contract_commitment: vector, - } - /// `Warehouse` object which stores NFTs of type `T` /// /// The reason that the type is limited is to easily support random @@ -297,49 +276,6 @@ module nft_protocol::warehouse { transfer::public_transfer(nft, tx_context::sender(ctx)); } - /// Create a new `RedeemCommitment` - /// - /// Contract commitment must be unfeasible to predict by the transaction - /// sender. The underlying value of the commitment can be pseudo-random as - /// long as it is not predictable by the validator. - /// - /// #### Panics - /// - /// Panics if commitment is not 32 bytes. - public fun new_redeem_commitment( - hashed_sender_commitment: vector, - ctx: &mut TxContext, - ): RedeemCommitment { - assert!( - vector::length(&hashed_sender_commitment) != 32, - EINVALID_COMMITMENT_LENGTH, - ); - - RedeemCommitment { - id: object::new(ctx), - hashed_sender_commitment, - contract_commitment: pseudorandom::rand_with_ctx(ctx), - } - } - - /// Creates a new `RedeemCommitment` and transfers it to the transaction - /// caller. - /// - /// Contract commitment must be unfeasible to predict by the transaction - /// caller. The underlying value of the commitment can be pseudo-random as - /// long as it is not predictable by the validator. - /// - /// #### Panics - /// - /// Panics if commitment is not 32 bytes. - public entry fun init_redeem_commitment( - hashed_sender_commitment: vector, - ctx: &mut TxContext, - ) { - let commitment = new_redeem_commitment(hashed_sender_commitment, ctx); - transfer::transfer(commitment, tx_context::sender(ctx)); - } - /// Randomly redeems NFT from `Warehouse` /// /// Requires a `RedeemCommitment` created by the user in a separate @@ -363,23 +299,8 @@ module nft_protocol::warehouse { user_commitment: vector, ctx: &mut TxContext, ): T { - let supply = supply(warehouse); - assert!(supply != 0, EEMPTY); - - // Verify user commitment - let RedeemCommitment { - id, - hashed_sender_commitment, - contract_commitment - } = commitment; - - object::delete(id); - - let user_commitment = std::hash::sha3_256(user_commitment); - assert!( - user_commitment == hashed_sender_commitment, - EINVALID_COMMITMENT, - ); + let (_, contract_commitment) = + redeem_random::consume_commitment(commitment, user_commitment); // Construct randomized index let supply = supply(warehouse); @@ -426,17 +347,6 @@ module nft_protocol::warehouse { object::delete(id); } - /// Destroyes `RedeemCommitment` - public entry fun destroy_commitment(commitment: RedeemCommitment) { - let RedeemCommitment { - id, - hashed_sender_commitment: _, - contract_commitment: _, - } = commitment; - - object::delete(id); - } - // === Getter Functions === /// Return how many `Nft` there are to sell From dddb462d274d9b8e3e4689e3c9197774a0370726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Wed, 19 Apr 2023 17:07:49 +0200 Subject: [PATCH 2/3] Pass through user commitment --- .../sources/config/redeem/strategy.move | 18 +++++++++++++++--- launchpad_v2/sources/inventory/warehouse.move | 6 ++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/launchpad_v2/sources/config/redeem/strategy.move b/launchpad_v2/sources/config/redeem/strategy.move index 4d52b1fe..c225da9e 100644 --- a/launchpad_v2/sources/config/redeem/strategy.move +++ b/launchpad_v2/sources/config/redeem/strategy.move @@ -91,11 +91,19 @@ module launchpad_v2::redeem_strategy { struct ParametersKey has copy, drop, store {} + struct RandomCommitment has store { + commitment: RedeemCommitment, + user_commitment: vector, + } + public fun register_parameters_random( object: &mut UID, commitment: RedeemCommitment, + user_commitment: vector, ) { - add_parameters(object, commitment) + add_parameters( + object, RandomCommitment { commitment, user_commitment }, + ) } public fun register_parameters_by_index(object: &mut UID, index: u64) { @@ -106,8 +114,12 @@ module launchpad_v2::redeem_strategy { add_parameters(object, id) } - public fun extract_parameters_random(object: &mut UID): RedeemCommitment { - remove_parameters(object) + public fun extract_parameters_random( + object: &mut UID, + ): (RedeemCommitment, vector) { + let commitment: RandomCommitment = remove_parameters(object); + let RandomCommitment { commitment, user_commitment } = commitment; + (commitment, user_commitment) } public fun extract_parameters_by_index(object: &mut UID): u64 { diff --git a/launchpad_v2/sources/inventory/warehouse.move b/launchpad_v2/sources/inventory/warehouse.move index 1eabda6c..a00c82e2 100644 --- a/launchpad_v2/sources/inventory/warehouse.move +++ b/launchpad_v2/sources/inventory/warehouse.move @@ -108,12 +108,10 @@ module launchpad_v2::warehouse { } else if (redeem_strategy::is_pseudorandom(strategy)) { redeem_pseudorandom_nft(warehouse, ctx) } else if (redeem_strategy::is_random(strategy)) { - let commitment = redeem_strategy::extract_parameters_random( + let (commitment, user_commitment) = redeem_strategy::extract_parameters_random( venue::cert_uid_mut(&mut certificate), ); - // TODO: Figure out best way to pass user commitment - // for now its `vector::empty()` - redeem_random_nft(warehouse, commitment, vector::empty(), ctx) + redeem_random_nft(warehouse, commitment, user_commitment, ctx) } else if (redeem_strategy::is_by_index(strategy)) { let index = redeem_strategy::extract_parameters_by_index( venue::cert_uid_mut(&mut certificate), From 2a60ecd3f834778ad9d9514fe937bd19cd37ecbc Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:12:32 +0100 Subject: [PATCH 3/3] Merge remote-tracking branch 'origin/develop' into feature/safer-redeem --- contracts/launchpad/sources/warehouse.move | 99 +++++++++++++++++-- .../sources/inventory/warehouse.move | 46 ++++++++- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/contracts/launchpad/sources/warehouse.move b/contracts/launchpad/sources/warehouse.move index 6f65fbc8..378270b2 100644 --- a/contracts/launchpad/sources/warehouse.move +++ b/contracts/launchpad/sources/warehouse.move @@ -16,8 +16,6 @@ module launchpad::warehouse { use sui::tx_context::{Self, TxContext}; use sui::object::{Self, ID , UID}; - use nft_protocol::redeem_random::{Self, RedeemCommitment}; - use originmate::pseudorandom; /// `Warehouse` does not have NFTs left to withdraw @@ -41,6 +39,29 @@ module launchpad::warehouse { /// Call `warehouse::redeem_nft_with_id` with an ID that exists. const EINVALID_NFT_ID: u64 = 4; + /// Attempted to construct a `RedeemCommitment` with a hash length + /// different than 32 bytes + const EINVALID_COMMITMENT_LENGTH: u64 = 5; + + /// Commitment in `RedeemCommitment` did not match original value committed + /// + /// Call `warehouse::random_redeem_nft` with the correct commitment. + const EINVALID_COMMITMENT: u64 = 6; + + /// Used for the client to commit a pseudo-random + struct RedeemCommitment has key { + /// `RedeemCommitment` ID + id: UID, + /// Hashed sender commitment + /// + /// Sender will have to provide the pre-hashed value to be able to use + /// this `RedeemCommitment`. This value can be pseudo-random as long + /// as it is not predictable by the validator. + hashed_sender_commitment: vector, + /// Open commitment made by validator + contract_commitment: vector, + } + /// `Warehouse` object which stores NFTs of type `T` /// /// The reason that the type is limited is to easily support random @@ -276,6 +297,49 @@ module launchpad::warehouse { transfer::public_transfer(nft, tx_context::sender(ctx)); } + /// Create a new `RedeemCommitment` + /// + /// Contract commitment must be unfeasible to predict by the transaction + /// sender. The underlying value of the commitment can be pseudo-random as + /// long as it is not predictable by the validator. + /// + /// #### Panics + /// + /// Panics if commitment is not 32 bytes. + public fun new_redeem_commitment( + hashed_sender_commitment: vector, + ctx: &mut TxContext, + ): RedeemCommitment { + assert!( + vector::length(&hashed_sender_commitment) != 32, + EINVALID_COMMITMENT_LENGTH, + ); + + RedeemCommitment { + id: object::new(ctx), + hashed_sender_commitment, + contract_commitment: pseudorandom::rand_with_ctx(ctx), + } + } + + /// Creates a new `RedeemCommitment` and transfers it to the transaction + /// caller. + /// + /// Contract commitment must be unfeasible to predict by the transaction + /// caller. The underlying value of the commitment can be pseudo-random as + /// long as it is not predictable by the validator. + /// + /// #### Panics + /// + /// Panics if commitment is not 32 bytes. + public entry fun init_redeem_commitment( + hashed_sender_commitment: vector, + ctx: &mut TxContext, + ) { + let commitment = new_redeem_commitment(hashed_sender_commitment, ctx); + transfer::transfer(commitment, tx_context::sender(ctx)); + } + /// Randomly redeems NFT from `Warehouse` /// /// Requires a `RedeemCommitment` created by the user in a separate @@ -299,13 +363,25 @@ module launchpad::warehouse { user_commitment: vector, ctx: &mut TxContext, ): T { - let (_, contract_commitment) = - redeem_random::consume_commitment(commitment, user_commitment); - - // Construct randomized index let supply = supply(warehouse); assert!(supply != 0, EEMPTY); + // Verify user commitment + let RedeemCommitment { + id, + hashed_sender_commitment, + contract_commitment + } = commitment; + + object::delete(id); + + let user_commitment = std::hash::sha3_256(user_commitment); + assert!( + user_commitment == hashed_sender_commitment, + EINVALID_COMMITMENT, + ); + + // Construct randomized index vector::append(&mut user_commitment, contract_commitment); // Use supply of `Warehouse` as a additional nonce factor vector::append(&mut user_commitment, sui::bcs::to_bytes(&supply)); @@ -347,6 +423,17 @@ module launchpad::warehouse { object::delete(id); } + /// Destroyes `RedeemCommitment` + public entry fun destroy_commitment(commitment: RedeemCommitment) { + let RedeemCommitment { + id, + hashed_sender_commitment: _, + contract_commitment: _, + } = commitment; + + object::delete(id); + } + // === Getter Functions === /// Return how many `Nft` there are to sell diff --git a/contracts/launchpad_v2/sources/inventory/warehouse.move b/contracts/launchpad_v2/sources/inventory/warehouse.move index a00c82e2..7cf9ca12 100644 --- a/contracts/launchpad_v2/sources/inventory/warehouse.move +++ b/contracts/launchpad_v2/sources/inventory/warehouse.move @@ -16,8 +16,6 @@ module launchpad_v2::warehouse { use sui::tx_context::{Self, TxContext}; use sui::object::{Self, ID , UID}; - use nft_protocol::redeem_random::{Self, RedeemCommitment}; - use launchpad_v2::venue::{Self, NftCert}; use launchpad_v2::redeem_strategy; @@ -46,6 +44,30 @@ module launchpad_v2::warehouse { const EUnsupportedRedeemStrategy: u64 = 5; + /// Attempted to construct a `RedeemCommitment` with a hash length + /// different than 32 bytes + const EInvalidCommitmentLength: u64 = 6; + + /// Commitment in `RedeemCommitment` did not match original value committed + /// + /// Call `warehouse::random_redeem_nft` with the correct commitment. + const EInvalidCommitment: u64 = 7; + + /// Used for the client to commit a pseudo-random + struct RedeemCommitment has key { + /// `RedeemCommitment` ID + id: UID, + /// Hashed sender commitment + /// + /// Sender will have to provide the pre-hashed value to be able to use + /// this `RedeemCommitment`. This value can be pseudo-random as long + /// as it is not predictable by the validator. + hashed_sender_commitment: vector, + /// Open commitment made by validator + contract_commitment: vector, + } + + struct Witness has drop {} /// `Warehouse` object which stores NFTs of type `T` @@ -284,13 +306,29 @@ module launchpad_v2::warehouse { user_commitment: vector, ctx: &mut TxContext, ): T { - let (_, contract_commitment) = - redeem_random::consume_commitment(commitment, user_commitment); + // let (_, contract_commitment) = + // redeem_random::consume_commitment(commitment, user_commitment); // Construct randomized index let supply = supply(warehouse); assert!(supply != 0, EEmpty); + // Verify user commitment + let RedeemCommitment { + id, + hashed_sender_commitment, + contract_commitment + } = commitment; + + object::delete(id); + + let user_commitment = std::hash::sha3_256(user_commitment); + assert!( + user_commitment == hashed_sender_commitment, + EInvalidCommitment, + ); + + // Construct randomized index vector::append(&mut user_commitment, contract_commitment); // Use supply of `Warehouse` as a additional nonce factor vector::append(&mut user_commitment, sui::bcs::to_bytes(&supply));