From dce45328757efa5b4b6f63bb60b6af9ffdeb0ca7 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:17:13 +0100 Subject: [PATCH 1/3] init --- .../sources/config/auth/auth.move | 42 ++++++++++++ .../sources/{ => policies}/auth_request.move | 0 .../sources/policies/proceeds_policy.move | 68 +++++++++++++++++++ .../sources/{ => proceeds}/proceeds.move | 0 4 files changed, 110 insertions(+) rename contracts/launchpad_v2/sources/{ => policies}/auth_request.move (100%) create mode 100644 contracts/launchpad_v2/sources/policies/proceeds_policy.move rename contracts/launchpad_v2/sources/{ => proceeds}/proceeds.move (100%) diff --git a/contracts/launchpad_v2/sources/config/auth/auth.move b/contracts/launchpad_v2/sources/config/auth/auth.move index 6288e73f..b7f7fe47 100644 --- a/contracts/launchpad_v2/sources/config/auth/auth.move +++ b/contracts/launchpad_v2/sources/config/auth/auth.move @@ -57,6 +57,48 @@ module launchpad_v2::launchpad_auth { df::add(venue_uid, PubkeyDfKey {}, pubkey); } + public fun transfer( + source_kiosk: address, + destination: address, + msg: &vector, + signature: &vector, + hash: u8, + authority: address, + auth_list: &AuthList, + ) { + let pubkey = df::borrow(auth_list, PubkeyDfKey { authority }); + + assert!( + ecdsa_k1::secp256k1_verify(signature, &pubkey.key, msg, hash), + EINCORRECT_SIGNATURE + ); + + // Assert message has correct address and counter + let bcs_msg = bcs::new(*msg); + let counter = bcs::peel_u64(&mut bcs_msg); + + assert!( + counter == pubkey.counter, + EINCORRECT_MESSAGE_COUNTER + ); + + let sender = bcs::peel_address(&mut bcs_msg); + let receiver = bcs::peel_address(&mut bcs_msg); + + assert!( + sender == tx_context::sender(ctx), + EINCORRECT_MESSAGE_SENDER + ); + + assert!( + receiver == destination, + EINCORRECT_MESSAGE_RECEIVER + ); + + + + } + /// Verifies if a given message sent by the user has been signed by the /// venue authority (i.e. Creator/Marketplace client). /// diff --git a/contracts/launchpad_v2/sources/auth_request.move b/contracts/launchpad_v2/sources/policies/auth_request.move similarity index 100% rename from contracts/launchpad_v2/sources/auth_request.move rename to contracts/launchpad_v2/sources/policies/auth_request.move diff --git a/contracts/launchpad_v2/sources/policies/proceeds_policy.move b/contracts/launchpad_v2/sources/policies/proceeds_policy.move new file mode 100644 index 00000000..a870d53a --- /dev/null +++ b/contracts/launchpad_v2/sources/policies/proceeds_policy.move @@ -0,0 +1,68 @@ +module launchpad_v2::proceeds_request { + use nft_protocol::request::{Self, RequestBody, Policy, PolicyCap}; + use nft_protocol::witness; + use sui::object::{Self, ID}; + use sui::tx_context::{TxContext, sender}; + + // === Error === + + const EPolicyMismatch: u64 = 1; + + // === Structs === + + struct Witness has drop {} + struct AUTH_REQUEST has drop {} + + struct AuthRequest { + policy_id: ID, + sender: address, + venue_id: ID, + inner: RequestBody + } + + // === Fns === + + /// Construct a new `Request` hot potato which requires an + /// approving action from the policy creator to be destroyed / resolved. + public fun new( + venue_id: ID, policy: &Policy, ctx: &mut TxContext, + ): AuthRequest { + AuthRequest { + policy_id: object::id(policy), + sender: sender(ctx), + venue_id, + inner: request::new(ctx), + } + } + + public fun init_policy(ctx: &mut TxContext): (Policy, PolicyCap) { + request::new_policy(witness::from_witness(Witness {}), ctx) + } + + /// Adds a `Receipt` to the `Request`, unblocking the request and + /// confirming that the policy requirements are satisfied. + public fun add_receipt(self: &mut AuthRequest, rule: &Rule) { + request::add_receipt(&mut self.inner, rule); + } + + public fun inner_mut(self: &mut AuthRequest): &mut RequestBody { + &mut self.inner + } + + public fun confirm(self: AuthRequest, policy: &Policy) { + let AuthRequest { + policy_id, + sender: _, + venue_id: _, + inner, + } = self; + assert!(policy_id == object::id(policy), EPolicyMismatch); + request::confirm(inner, policy); + } + + public fun venue_id(self: &AuthRequest): ID { self.venue_id } + + public fun auth_sender(self: &AuthRequest): address { self.sender } + + public fun policy_id(self: &AuthRequest): ID { self.policy_id } +} diff --git a/contracts/launchpad_v2/sources/proceeds.move b/contracts/launchpad_v2/sources/proceeds/proceeds.move similarity index 100% rename from contracts/launchpad_v2/sources/proceeds.move rename to contracts/launchpad_v2/sources/proceeds/proceeds.move From 1c7bfb38a47b02b0a0644a2d8980160619b67b69 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:02:16 +0100 Subject: [PATCH 2/3] Remove clutter --- .../sources/config/auth/auth.move | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/contracts/launchpad_v2/sources/config/auth/auth.move b/contracts/launchpad_v2/sources/config/auth/auth.move index b7f7fe47..6288e73f 100644 --- a/contracts/launchpad_v2/sources/config/auth/auth.move +++ b/contracts/launchpad_v2/sources/config/auth/auth.move @@ -57,48 +57,6 @@ module launchpad_v2::launchpad_auth { df::add(venue_uid, PubkeyDfKey {}, pubkey); } - public fun transfer( - source_kiosk: address, - destination: address, - msg: &vector, - signature: &vector, - hash: u8, - authority: address, - auth_list: &AuthList, - ) { - let pubkey = df::borrow(auth_list, PubkeyDfKey { authority }); - - assert!( - ecdsa_k1::secp256k1_verify(signature, &pubkey.key, msg, hash), - EINCORRECT_SIGNATURE - ); - - // Assert message has correct address and counter - let bcs_msg = bcs::new(*msg); - let counter = bcs::peel_u64(&mut bcs_msg); - - assert!( - counter == pubkey.counter, - EINCORRECT_MESSAGE_COUNTER - ); - - let sender = bcs::peel_address(&mut bcs_msg); - let receiver = bcs::peel_address(&mut bcs_msg); - - assert!( - sender == tx_context::sender(ctx), - EINCORRECT_MESSAGE_SENDER - ); - - assert!( - receiver == destination, - EINCORRECT_MESSAGE_RECEIVER - ); - - - - } - /// Verifies if a given message sent by the user has been signed by the /// venue authority (i.e. Creator/Marketplace client). /// From 515969b71d01a6a1892a0c0cf22aa8ae2aee46e2 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:09:07 +0100 Subject: [PATCH 3/3] BPS fees --- .../sources/proceeds/bps_fees.move | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 contracts/launchpad_v2/sources/proceeds/bps_fees.move diff --git a/contracts/launchpad_v2/sources/proceeds/bps_fees.move b/contracts/launchpad_v2/sources/proceeds/bps_fees.move new file mode 100644 index 00000000..5db2e3d2 --- /dev/null +++ b/contracts/launchpad_v2/sources/proceeds/bps_fees.move @@ -0,0 +1,202 @@ +module nft_protocol::bps_fee { + use nft_protocol::collection::{Self, Collection}; + use nft_protocol::ob_transfer_request::{Self, TransferRequest, BalanceAccessCap}; + use nft_protocol::request::{Self, Policy, PolicyCap, WithNft}; + use nft_protocol::royalty; + use nft_protocol::utils; + use sui::transfer_policy::{TransferPolicyCap, TransferPolicy}; + use nft_protocol::witness::{Witness as DelegatedWitness}; + use originmate::balances::{Self, Balances}; + use std::fixed_point32; + use std::option::{Self, Option}; + use sui::balance::{Self, Balance}; + use sui::object::{Self, UID}; + use sui::transfer::share_object; + use sui::tx_context::{sender, TxContext}; + + /// === Errors === + + /// If the strategy has `is_enabled` set to false, cannot confirm any + /// `TransferRequest`.s + const ENotEnabled: u64 = 1; + + /// === Structs === + + /// A shared object which can be used to add receipts of type + /// `BpsFeeStrategyRule` to `TransferRequest`. + struct BpsFeeStrategy has key { + id: UID, + /// Royalty charged on trades in basis points + fee_bps: u16, + /// Allows this middleware to touch the balance paid. + /// The balance is deducted from the transfer request. + /// See the docs for `BalanceAccessCap` for more info. + access_cap: Option>, + /// Contains balances of various currencies. + aggregator: Balances, + /// If set to false, won't give receipts to `TransferRequest`. + is_enabled: bool, + } + + /// Rule for `TransferPolicy` to check that the fee has been paid. + /// Only `TransferRequest` with a receipt from this rule are allowed to + /// pass such policy. + struct BpsFeeStrategyRule has drop {} + + /// See the `nft_protocol::witness` module for obtaining the witness. + /// + /// Creates a new strategy which can be then shared with `share` method. + /// Optionally, add balance access policy + public fun new( + witness: DelegatedWitness, + collection: &mut Collection, + fee_bps: u16, + ctx: &mut TxContext, + ): BpsFeeStrategy { + let id = object::new(ctx); + + let domain = royalty::borrow_domain_mut( + collection::borrow_uid_mut(witness, collection), + ); + + royalty::add_strategy(domain, object::uid_to_inner(&id)); + + BpsFeeStrategy { + id, + is_enabled: true, + fee_bps, + access_cap: option::none(), + aggregator: balances::new(ctx), + } + } + + public fun share(self: BpsFeeStrategy) { share_object(self) } + + public fun add_balance_access_cap( + self: &mut BpsFeeStrategy, + cap: BalanceAccessCap, + ) { self.access_cap = option::some(cap); } + + public fun drop_balance_access_cap( + _witness: DelegatedWitness, + self: &mut BpsFeeStrategy, + ) { self.access_cap = option::none(); } + + public fun enable( + _witness: DelegatedWitness, + self: &mut BpsFeeStrategy, + ) { self.is_enabled = true; } + + /// Can't issue receipts for `TransferRequest` anymore. + public fun disable( + _witness: DelegatedWitness, + self: &mut BpsFeeStrategy, + ) { self.is_enabled = false; } + + /// Registers collection to use `BpsFeeStrategy` during the transfer. + public fun enforce(policy: &mut TransferPolicy, cap: &TransferPolicyCap) { + ob_transfer_request::add_originbyte_rule( + BpsFeeStrategyRule {}, policy, cap, false, + ); + } + + public fun drop(policy: &mut TransferPolicy, cap: &TransferPolicyCap) { + ob_transfer_request::remove_originbyte_rule( + policy, cap + ); + } + + /// Registers collection to use `BpsFeeStrategy` during the transfer. + public fun enforce_(policy: &mut Policy>, cap: &PolicyCap) { + request::enforce_rule_no_state, BpsFeeStrategyRule>(policy, cap); + } + + public fun drop_(policy: &mut Policy>, cap: &PolicyCap) { + request::drop_rule_no_state, BpsFeeStrategyRule>(policy, cap); + } + + /// Transfers the royalty to the collection royalty aggregator. + public fun collect_royalties( + collection: &mut Collection, strategy: &mut BpsFeeStrategy, + ) { + let balance = balances::borrow_mut(&mut strategy.aggregator); + let amount = balance::value(balance); + royalty::collect_royalty(collection, balance, amount); + } + + // TODO: add for generic request body + /// Uses the balance associated with the request to deduct royalty. + public fun confirm_transfer( + self: &mut BpsFeeStrategy, req: &mut TransferRequest, + ) { + assert!(self.is_enabled, ENotEnabled); + + let cap = option::borrow(&self.access_cap); + let (paid, _) = ob_transfer_request::paid_in_ft_mut(req, cap); + let fee_amount = calculate(self, balance::value(paid)); + balances::take_from(&mut self.aggregator, paid, fee_amount); + + ob_transfer_request::add_receipt(req, BpsFeeStrategyRule {}); + } + + // TODO: add for generic request body + /// Instead of using the balance associated with the `TransferRequest`, + /// pay the royalty in the given token. + public fun confirm_transfer_with_balance( + self: &mut BpsFeeStrategy, + req: &mut TransferRequest, + wallet: &mut Balance, + ) { + assert!(self.is_enabled, ENotEnabled); + + let (paid, _) = ob_transfer_request::paid_in_ft(req); + let fee_amount = calculate(self, paid); + balances::take_from(&mut self.aggregator, wallet, fee_amount); + + ob_transfer_request::add_receipt(req, BpsFeeStrategyRule {}); + } + + public fun fee_bps(self: &BpsFeeStrategy): u16 { + self.fee_bps + } + + public fun calculate(self: &BpsFeeStrategy, amount: u64): u64 { + // TODO: Need to consider implementing Decimals module for increased + // precision, or wait for native support + let royalty_rate = fixed_point32::create_from_rational( + (fee_bps(self) as u64), + (utils::bps() as u64) + ); + + fixed_point32::multiply_u64( + amount, + royalty_rate, + ) + } + + // === Helpers === + + /// 1. Creates a new `RoyaltyDomain` + /// 2. Assigns it to the collection + /// 3. Creates a new shared `BpsFeeStrategy` + /// 4. Assigns it to the domain + /// + /// The creator is the sender. + /// The strategy has access to `TransferRequest` balance + public fun create_domain_and_add_strategy( + witness: DelegatedWitness, + collection: &mut Collection, + bps: u16, + ctx: &mut TxContext, + ) { + let royalty_domain = royalty::from_address(sender(ctx), ctx); + collection::add_domain(witness, collection, royalty_domain); + + let royalty_strategy = new(witness, collection, bps, ctx); + add_balance_access_cap( + &mut royalty_strategy, + ob_transfer_request::grant_balance_access_cap(witness), + ); + share(royalty_strategy); + } +}