Joystream / joystream

Joystream Monorepo
http://www.joystream.org
GNU General Public License v3.0
1.43k stars 115 forks source link

Creator Token Workload #3382

Closed ignazio-bovo closed 2 years ago

ignazio-bovo commented 2 years ago

Notes

Questions

  1. What do we do when a $CRT holder gets removed from whitelist in a Permissioned Transfer policy? Can he still transact with whitelisted members?

    Open problems / Comments

    • establish feasibility of using the substrate asset pallet
    • can we eventually add vesting feature to the substrate assets pallet?
bedeho commented 2 years ago
bedeho commented 2 years ago

Assets

Note: If you are going to update the plan, its better to reply with a new plan, rather than edit the old one, other wise the comment history makes 0 sense.

Questions

What do we do when a $CRT holder gets removed from whitelist in a Permissioned Transfer policy? Can he still transact with whitelisted members?

Removal from whitelist is not supported, it was not part of the requirements.r

bedeho commented 2 years ago

Looking a bit at XCM, its not actually clear to me that we could not just implement our own minimal asset module inside project_token, with exactly the features we need, and then configure the XCM executor to work with it. It is not however clear to me how well integrations with wallets and block explorers will work.

bedeho commented 2 years ago

After looking closer at XCM, it seems to me that we are better off just implementing our own fungible token inside of the project_token pallet, because

There would possibly be some friction with getting full support at the browser extenstion level (so that the extension would list the different users assets), however, as long basic features around this are present in the apps we care most about (Atlas, Pioneer, etc.), this is acceptable, and I am sure we can get those integrations later.

ignazio-bovo commented 2 years ago

Project Token Design ====================

Table of Contents

  1. Intro
    1. Notation
    2. Document organization
  2. File structure src/
    1. trait.rs
    2. types.rs
    3. tests
      1. mod.rs
      2. mock.rs
      3. patronage.rs
      4. transfer.rs
      5. sale.rs
      6. revenue_split.rs
      7. ibco.rs
      8. canonical.rs
    4. lib.rs
    5. errors.rs
    6. events.rs
  3. Storage State components
  4. Auxiliary types
    1. TokenData
    2. InitialOfferingState Implementation
    3. TransmissionPolicy
    4. TransferLocation Implementations
      1. TransferLocationSimple
      2. ValidatedTransferLocation
    5. Sale
  5. APIs
    1. MultiCurrencyBase
      1. Declaration
      2. Invariants
      3. issue_token(initial_issuance: Balance)
      4. deissue_token(token_id: Self::TokenId)
      5. balance_of(token_id: TokenId, account_id: SourceLocation) -> Result<Balance, Error>
      6. issuance(token_id: TokenId) -> Result<Balance, Error>
      7. burn(token_id: TokenId, amount: Balance) -> Result<(), Error>
      8. mint(token_id: TokenId, amount: Balance) -> Result<(), Error>
      9. slash(token_id: TokenId, account_id: SourceLocation, amount: Balance) -> Result<(), Error>
      10. deposit(token_id: TokenId, account_id: DestinationLocation, amount: Balance) -> Result<(), Error>
      11. transfer(token_id: TokenId, source: SourceLocation, destination: DestinationLocation, amount: Balance) -> Result<(), Error>
    2. ReservableMultiCurrency
      1. Declaration
      2. Invariants
      3. reserve(token_id: TokenId, account_id: AccountId, amount: Balance)
      4. unreserve(token_id: TokenId, account_id: AccountId, amount: Balance)
    3. AdminAuthenticator
      1. Declaration
      2. Invariants
    4. InitialOfferingState
      1. is_idle(self) -> bool
      2. is_sale(self) -> bool
      3. is_ibco(self) -> bool
      4. change_to_idle(self) -> bool
      5. change_to_sale(self) -> bool
      6. change_to_ibco(self) -> bool
      7. Extrinsic version
    5. TransferLocation
      1. Declaration
      2. is_valid_location_for_policy(policy: TransferPolicy)
      3. location_account()
    6. TransferPermissionPolicy
      1. Declaration
      2. can_transfer_to(location: TransferLocation) -> bool
      3. is_permissionless(&self) -> bool
      4. is_permissioned(&self) -> bool
      5. change_to_permissionless(self) -> Self
      6. change_to_permissioned(self, whitelist_commitment: Hash) -> Self
      7. change_state_to_permissionless(origin, token_id) -> Result<(), Error>
      8. change_state_to_permissioned(origin, whitelist_commitment: Hash) -> Result<(), Error>
    7. Transfer
      1. transfer(token_id: TokenId, source: AccountId, destination: TransferLocation, amount: Balance)
      2. transfer_multi_output(token_id: TokenId, source: AccountId, outputs: Vec<(TransferLocation, Balance)>)
      3. transfer(origin, token_id: TokenId, destination: TransferLocation, amount: Balance)
      4. transfer_multi_output(token_id: TokenId, source: AccountId, outputs: Vec<(TransferLocation, Balance)>)
    8. RevenueSplit
      1. Declaration
      2. issue_revenue_split(origin, token_id: Self::TokenId, start: BlockNumber, duration: BlockNumber, percentage: Perbill)
      3. partake(origin, split_id: Self::SplitId, start: BlockNumber, amount_to_stake: Self::Balance)
      4. claim_revenue(origin, split_id: Self::SplitId) -> Result<(), Error>
    9. Patronage
      1. Declaration
      2. mint(token_id: TokenId, amount: Balance) -> Result<(), Error>
      3. claim_creator_credit(origin, token_id: TokenId) -> Result<(), Error>
      4. change_patronage_rate(origin, token_id: TokenId, new_rate: PerBill) -> Result<(), Error>
    10. BondingCurveShape
      1. Invariants
      2. bonding_function(input_amount: Balance, input_reserve: Balance)
      3. unbonding_function(output_amount: Balance, output_reserve: Balance)
    11. IBCO
      1. Declaration
      2. Invariants
      3. bond(origin, token_id: TokenId, to_account: TransferLocation<AccountId>, amount: ReserveBalance)
      4. unbond(origin, token_id: TokenId, from_account: AccountId, amount: OutputBalance)
    12. Sale
      1. Declaration
      2. issue_sale(origin, token_id: Self::TokenId, start: BlockNumber, duration: BlockNumber, exchange_rate: Balance, whitelist_commitment: Hash, cap: Balance, locked: Balance)
      3. update_sale_start(origin, token_id: Self::TokenId, sale_id: Self::SaleId, start: BlockNumber)
      4. update_sale_duration(origin, sale_id: Self::SaleId, duration: BlockNumber)
      5. puchase(origin, token_id: TokenId, sale_id: SaleId, to_location: TransferLocation<AccountId>)
  6. References

Intro

This is design document for the CRT pallet. It is also meant to be a reference for the implementation and auxiliary document for the PRs reviewer(s). Implementation will try to follow this design as close as possible (once approved). There will be for sure some mistakes/typos/unclear things, just point them out and I will try to update the document.

Notation

Document organization

First I will describe the pallet file structure. Then I will introduce the actual storage state for the pallet. This will also helps to read the traits specifications later. the traits, these traits will be put in a separate trait.rs file and they will provide the behaviour that the ProjectToken Pallet will implement Along with the Traits methods I have put some design by contract specifications, which will provide intuition about the actual implementation I am planning to write. (Keep in mind that here I have just a prototype for the pallet, actual implementation might slightly vary).

File structure src/

trait.rs

Where the trait declaration will go

types.rs

Where auxiliary types eg TokenData struct will go

tests

Folder where tests will go, in particular:

mod.rs

Rust module file including the test files

mock.rs

Mock-test scenario setup, along with helper functions

patronage.rs

Tests related to the minting and ensuring that the proper patronage policy will comply

transfer.rs

Tests related to the minting and ensuring that the policy transfer will comply.

sale.rs

Tests for the sale functionality

revenue_split.rs

Tests for the revenue split functionality

ibco.rs

Tests for the initial bonding curve offering functionality

canonical.rs

Tests for the standard MultiCurrencyBase and ReservableMultiCurrency traits implementation. In particular issuing, burning (with appropriate existential deposit checks in place).

lib.rs

Will contain the state storage layout and the trait implementations for the Pallet::Module<T> and the trait configuration

errors.rs

Error enum definitions

events.rs

Events enum definition, I would split events since, quite frankly the lib.rs will probably be a very long file and it will be easier (for a QN integration viewpoint) to just navigate between the events.

Storage State components

Auxiliary types

Here I sketch the idea for possible auxiliary types

TokenData

Record for the unique characteristics of a token

InitialOfferingState Implementation

enum

TransmissionPolicy

Enum containing either:

Implements the TransferPermissionPolicy Trait

TransferLocation Implementations

TransferLocationSimple

simple struct wrapper around an account Id for which is_valid_location_for_policy gives always true

ValidatedTransferLocation

record of:

Sale

record containing the characteristic info. for a token sale

APIs

MultiCurrencyBase

Encapsulate the basic business logic (i.e. primitives) for a currency. This trait provides functionality for:

Issuance and supply for a token are used intercheangebly. Since I am not introducing the orthogonal concept of reservable currency here balance_of is meant to be intended as free balance.

Declaration

Implemented in the pallet Module. Originally I wanted to have one base implementation. and then decorate it in the by means of the Decorator pattern. I am no longer sure about that approach for two reason:

It was a nice application of the single responsibility principle however… Hereafter (and probably also in the codebase) base_transfer will denote a call the bare bones (undecorated) transfer primitive.

pub trait MultiCurrencyBase<SourceLocation, DestinationLocation = SourceLocation> {
    // types defined
    type TokenId;
    type Balance;

    // API
    fn issue_token(initial_issuance: Balance);
    fn burn(token_id: TokenId, amount: Balance) -> Result<(), Error>;
    fn mint(token_id: TokenId, amount: Balance) -> Result<(), Error>;
    fn slash(token_id: TokenId, account_id: SourceLocation, amount: Balance) -> Result<(), Error>;
    fn deposit(
        token_id: TokenId,
        account_id: DestinationLocation,
        amount: Balance
    ) -> Result<(), Error>;
    fn transfer(
        token_id: TokenId,
        source: SourceLocation,
        destination: DestinationLocation,
        amount: Balance,
    ) -> Result<(), Error>;
    fn balance_of(token_id: TokenId, account_id: SourceLocation) -> Result<Balance, Error>;
    fn issuance(token_id: TokenId) -> Result<Balance, Error>;
}

Invariants

issue_token(initial_issuance: Balance)

Issue a new token with the specified issuance and idle state. Several other similar method can be added like issue_token_with_sale(sale_parameters..) that sets the initial issuance state

  1. Preconditions

  2. Postconditions

    • TokenData struct added to the TokenDataById map in with Idle state and issuance set to the initial issuance provided

deissue_token(token_id: Self::TokenId)

De issue an existing token and remove all the existing account data.

  1. Preconditions

    • ensure_token_exists(token_id).is_ok())
  2. Postconditions

    • TokenDataById::remove(token_id)
    • AccountDataByTokenAndId::remove_prefix(token_id)
    • SaleDataByTokenAndId::remove_prefix(token_id)
    • SplitPartecipationByTokenAndId::remove_prefix(token_id)

balance_of(token_id: TokenId, account_id: SourceLocation) -> Result<Balance, Error>

Getter function for account_data.free_balance

  1. Preconditions

    • token_id must exists
    • account_id for token_id must exists

issuance(token_id: TokenId) -> Result<Balance, Error>

Getter function for token_data.current_issuance

  1. Preconditions

    • ensure_token_exists(token_id).is_ok()

burn(token_id: TokenId, amount: Balance) -> Result<(), Error>

Decreases of amount the token_Id current issuance

  1. Preconditions

    • ensure_token_exists(token_id).is_ok()
    • issuance(token_id) >= amount
  2. Postconditions

      • OLD[issuance(token_id)] - amount = issuance(token_id)

mint(token_id: TokenId, amount: Balance) -> Result<(), Error>

Increases of amount the token_Id current issuance

  1. Preconditions

    • ensure_token_exists(token_id).is_ok()
  2. Postconditions

      • OLD[issuance(token_id)] + amount = issuance(token_id)

slash(token_id: TokenId, account_id: SourceLocation, amount: Balance) -> Result<(), Error>

Slashes amount of token_Id from existng account_id

  1. Preconditions

    • token_id must exists
    • account_id for token_id must exists
    • balance_of(token_id, account_id) >= amount
  2. Postconditions

    • OLD[balance_of(token_id, account_id) - amount = balance_of(token_id, account_id)
    • if balance_of(token_id, account_id) is lower than existential deposit then POSTCONDITIONS[AccountDataByTokenAndId::remove(token_id, account_id)]

deposit(token_id: TokenId, account_id: DestinationLocation, amount: Balance) -> Result<(), Error>

Deposit token_id amount to an existing account_id. There should be also a deposit_creating counterpart that can be used to at least create accounts

  1. Preconditions

    • ensure_token_exists(token_id).is_ok()
    • matches!(Ok(account_data), ensure_account_data_exists(token_Id, account_id)
  2. Postconditions

    • OLD[_data.free_balance] - amount = account_data.free_balance

transfer(token_id: TokenId, source: SourceLocation, destination: DestinationLocation, amount: Balance) -> Result<(), Error>

Transfer amount of token_id between source and destination accounts

  1. Preconditions

    • token_id must exists
    • source account must exists for token_id
    • destination account must exists for token_id
    • balance_of(token_id, source) >= amount
  2. Postconditions

    • OLD[balance_of(token_id, source)] - amount = =balance_of(token_id, source)
    • OLD[balance_of(token_id, destination)] + amount = =balance_of(token_id, destination)

ReservableMultiCurrency

Encapsules the functionalities for reserving/unreserving amount's from account_id for token_id. Introduces the concepts of freebalance and reservebalance

Declaration

implemented in the pallet Module

pub trait ReservableMultiCurrency<AccountId> {
    // types defined
    type Balance;
    type TokenId;

    // API
    fn reserve(token_id: TokenId, account_id: AccountId, amount: Balance);
    fn unreserve(token_id: TokenId, account_id: AccountId, amount: Balance);
    fn reserved_balance(tokenid: TokenId, account_id: AccountId) -> Result<Balance, Error>;
    fn free_balance(tokenid: TokenId, account_id: AccountId) -> Result<Balance, Error>;
}

Invariants

reserve(token_id: TokenId, account_id: AccountId, amount: Balance)

  1. Preconditions

    • token_id exsists
    • account_id for token_id exists
    • free_balance(token_id, account_id) >= amount
  2. Postconditions

    • OLD[free_balance(token_id, account_id)] - amount = freebalance(tokenid, accountid)=
    • OLD[reserved_balance(token_id, account_id)] + amount = reservedbalance(tokenid, accountid)=

unreserve(token_id: TokenId, account_id: AccountId, amount: Balance)

  1. Preconditions

    • token_id exsists
    • account_id for token_id exists
    • Self::reserved_balance(token_id, account_id) >= amount
  2. Postconditions

    • OLD[free_balance(token_id, account_id)] + amount = freebalance(tokenid, accountid)=
    • OLD[reserved_balance(token_id, account_id)] - amount = reservedbalance(tokenid, accountid)=

AdminAuthenticator

Introduces the concept of administrator (for the token) I decoupled this since I am not sure how to implemnt it yet. Used to establish autentication between a signed origin and a token_id owner

Declaration

Implemented by the InitialOfferingState

pub trait AdminAuthenticator<AccountId, AdminId> {
    // API
    fn ensure_admin_authentication(origin, admin_id: AdminId) -> Result<(), Error>;
    fn admin_account(admin_id: AdminId) -> Result<AccountId, Error>;
}

Invariants

InitialOfferingState

State in which the token find itself for bootstrapping initial liquidity. One of the following. Idle, Sale, IBCO. Encapsulates the State pattern and proper transition functions

is_idle(self) -> bool

Predicate for the idle state

is_sale(self) -> bool

Predicate for the Sale state

is_ibco(self) -> bool

Predicate for the IBCO state

change_to_idle(self) -> bool

change to the idle state

  1. Preconditions

    • self.is_idle()
  2. Postconditions

    • self.is_idle()

change_to_sale(self) -> bool

Change to the Sale state

  1. Preconditions

    • self.is_idle() || self.is_sale()
  2. Postconditions

    • self.is_sale()

change_to_ibco(self) -> bool

Changes to the IBCO state

  1. Preconditions

    • self.is_idle() || self.is_sale() || self.is_ibco()
  2. Postconditions

    • self.is_ibco()

Extrinsic version

for each of the change_to_* there should be a change_to_*(origin, token_id, state_params...) implemented in the pallet Module, used to change TokenData state after owner authentication

TransferLocation

Declaration

pub trait TransferLocation<AccountId, TransferPolicy> {
    fn is_valid_location_for_policy(policy: TransferPolicy) -> bool;
    fn location_account() -> AccountId;
}

AccountId wrapper that interoperates with the transfer policy in order to validate transfer destination location. Encapsules the merkle proof logic in validating the location for the permissioned case. This component is the Visitor in the visitor pattern

is_valid_location_for_policy(policy: TransferPolicy)

  1. Preconditions

  2. Postconditions

    • policy.is_permissionless() || policiy.is_permissioned() && self.validate_location()

location_account()

getter for the wrapped account

TransferPermissionPolicy

Declaration

pub trait TransferPermissionPolicy<AccountId, Hash> {
    fn can_transfer_to(location: TransferLocation<AccountId>) -> bool;
    fn is_permissionless(&self) -> bool;
    fn is_permissioned(&self) -> bool;
    fn change_to_permissionless(self) -> Self;
    fn change_to_permissioned(self, whitelist_commitment: Hash) -> Self;
}

Encapsulates the state of the transfer policy (via the State pattern), also together with the TransferLocation trait it allows account destination validation via the visitor pattern.

can_transfer_to(location: TransferLocation) -> bool

Establish whether transfer location is allowed for the policy by means of the Visitor pattern

  1. Preconditions

  2. Postconditions

    • POSTCONDITIONS[location.is_valid_for_policy(self)]

is_permissionless(&self) -> bool

Predicate method for distinguishing permissionless state

is_permissioned(&self) -> bool

Predicate method for distinguishing permissioned state

change_to_permissionless(self) -> Self

Transition function to permissionless state

  1. Preconditions

  2. Postconditions

    • self.is_permissionless()

change_to_permissioned(self, whitelist_commitment: Hash) -> Self

Transition function to permissioned state with the given whitelist commitment

  1. Preconditions

    • self.is_permissionless()
  2. Postconditions

    • self.is_permissioned()

    Converting the last two to extrinsics (in order to allow the creator to change permission), these are not included in the trait but they will be implemented in the pallet::Module<T>.

change_state_to_permissionless(origin, token_id) -> Result<(), Error>

Transition function to permissionless state

  1. Preconditions

    -matches!(Ok(token_data), ensure_token_exists(token_id)) -PRECONDITIONS[token_data.state.change_to_permissionless]

  2. Postconditions

    -POSTCONDITIONS[token_data.state.change_to_permissionless]

change_state_to_permissioned(origin, whitelist_commitment: Hash) -> Result<(), Error>

Transition function to permissioned state with the given whitelist commitment

  1. Preconditions

    -matches!(Ok(token_data), ensure_token_exists(token_id)) -PRECONDITIONS[token_data.state.change_to_permissioned(white_list_commitment)]

  2. Postconditions

    -POSTCONDITIONS[token_data.state.change_to_permissioned(white_list_commitment)]

Transfer

Encapsulates:

The logic for validate destination accounts (i.e. the policy itself) is delegates to the TransferPermissionPolicy trait. Non source signed version are meant to be used as non-extrinsics calls (for example withing AMM extrinsics).

transfer(token_id: TokenId, source: AccountId, destination: TransferLocation, amount: Balance)

Transfer amount amount of token_id to account destination from signer account (source)

  1. Preconditions

    • PRECONDITIONS[MultiCurrency::base_transfer(token_id, source, destination, amount)]
    • matches!(Ok(policy), ensure_policy_for_token_exists(token_id))
    • policy.can_transfer_to(destination)
  2. Postconditions

    • POSTCONDITIONS[MultiCurrency::base_transfer(token_id, source, destination, amount)

transfer_multi_output(token_id: TokenId, source: AccountId, outputs: Vec<(TransferLocation, Balance)>)

Transfer amount outputs[i].1 of token_id to account outputs[i].0 from signer account (source)

  1. Preconditions

    • output.iter().all(|(destination, amount)| PRECONDITIONS[MultiCurrency::base_transfer(token_id, source, destination, amount)])
    • matches!(Ok(policy), ensure_policy_for_token_exists(token_id))
    • output.iter().all(|(destination, _)| policy.can_transfer_to(destination))
  2. Postconditions

    • outputs.iter().all(|(destination, amount) POSTCONDITIONS[MultiCurrency::transfer(token_id, source, destination, amount)])

transfer(origin, token_id: TokenId, destination: TransferLocation, amount: Balance)

Same as transfer with origin signed by the source account. This is meant to be used as extrinsic when two normal user wants to exchange CRT

transfer_multi_output(token_id: TokenId, source: AccountId, outputs: Vec<(TransferLocation, Balance)>)

Same as transfer_multi_output with origin signed by the source account. This is meant to be used as extrinsic when sevaral normal user wants to exchange CRT

RevenueSplit

Functionality for the desired revenue split

Declaration

pub trait RevenueSplit<AccountId> {
    type MultiCurrency: MultiCurrencyBase<AccountId> + ReservableMultiCurrency<AccountId>;
    type TokenId = MultiCurrency::TokenId;
    type Balace = MultiCurrency::Balance;
    type AdminAuthentication: AdminAuthenticator<Self::AccountId, Self::TokenId>;

    // provided types
    type SplitId: Parameter + Member + Copy + Default + FullCodec;

    fn issue_revenue_split(
        origin,
        token_id: Self::TokenId,
        start: BlockNumber,
        duration: BlockNumber,
        percentage: Perbill
    ) -> Result<(), Error>;

    fn partake(
        origin,
        split_id: Self::SplitId,
        start: BlockNumber,
        amount_to_stake: Self::Balance
    ) -> Result<(), Error>;

    fn claim_revenue(
        origin,
        split_id: Self::SplitId
    ) -> Result<(), Error>;
}

issue_revenue_split(origin, token_id: Self::TokenId, start: BlockNumber, duration: BlockNumber, percentage: Perbill)

  1. Preconditions

    • AdminAuthentication::ensure_admin_authentication(origin, token_id).is_ok()
    • System::block_number() <= start
    • !percentage.is_zero()
  2. Postconditions

    • OLD[next_split_id()] + 1 = next_split_id()
    • Revenue split with the specified chararacteristics is issued with id OLD[next_split_id()]

partake(origin, split_id: Self::SplitId, start: BlockNumber, amount_to_stake: Self::Balance)

  1. Preconditions

    • matches!(Ok(sender), ensure_signed(origin))
    • revenue Revenue split split_id must exists
    • PRECONDITIONS[MultiCurrency::base_stake(revenue.token_id, sender, amount_to_stake)]
  2. Postconditions

    • POSTCONDITIONS[MultiCurrency::base_stake(revenue.token_id, sender, amount_to_stake)]
    • (sender, amount_to_stake) added to ParticipationsByAccountAndId

claim_revenue(origin, split_id: Self::SplitId) -> Result<(), Error>

  1. Preconditions

    • matches!(Ok(sender), ensure_signed(origin))
    • matches!(Ok(split), ensure_split_exists(split_id))
    • matches!(Ok(revenue), ensure_split_partecipation_exists(sender, split_id))
  2. Postconditions

    • POSTCONDITIONS[MultiCurrency::base_transfer(split_id.token_id, split_id.source, sender, revenue)]

Patronage

This encapsulates the patronage mechanism. The actual implementation consists of decorating the MultiCurrencyBase::mint implementation function so that percentage of the amount is added to the creator credit. Creator can later cash out this credit with the effects that credit (in CRT) is minted to his account

Declaration

Implemented in pallet Module

pub trait Patronage<AccountId> {
    type MultiCurrency: MultiCurrency<AccountId>;
    type TokenId = MultiCurrency::TokenId;
    type AdminAuthentication: AdminAuthenticator<AccountId, Self::TokenId>;

    type Balance = MultiCurrency::Balance;
    type TokenId = MultiCurrency::TokenId;

    fn mint(token_id: TokenId, amount: Balance) -> Result<(), Error>;

    fn claim_creator_credit(origin, token_id: TokenId) -> Result<(), Error>;

    fn change_patronage_rate(origin, token_id: TokenId, new_rate: PerBill) -> Result<(), Error>;
}

mint(token_id: TokenId, amount: Balance) -> Result<(), Error>

Decorates the MultiCurrency::base_mint function and accounts for creator credit

  1. Preconditions

    • PRECONDITIONS[MultiCurrency::base_mint(token_id, amount)
  2. Postconditions

    • POSTCONDITIONS[MultiCurrency::base_mint(token_id, amount)
    • OLD[CreatorCredit(tokenf_id)] + credit = CreatorCredit(tokenid)= , credit is amonut * patronage_rate

claim_creator_credit(origin, token_id: TokenId) -> Result<(), Error>

Allow the token owner to claim his amounts (credit amount commit needed?)

  1. Preconditions

    • matches!(Ok(token_info), MultiCurrency::ensure_token_exists(token_id))
    • AdminAuthentication::ensure_admin_authentication(origin, token_id)
  2. Postconditions

    • CreatorCredit(token_id).is_zero()
    • OLD[MultiCurrency::balance_of(token_id, AdminAuthentication::admin_account(token_id))] + OLD[CreatorCredit(token_id)] = MultiCurrency::balencof(tokenid, AdminAuthentication::adminaccount(tokenid))=

change_patronage_rate(origin, token_id: TokenId, new_rate: PerBill) -> Result<(), Error>

Update the patronage rate for token,

  1. Preconditions

    • matches!(Ok(token_info), MultiCurrency::ensure_token_exists(token_id))
    • AdminAuthentication::ensure_admin_authentication(origin, token_id)
    • token_info.patronage_rate > new_rate
  2. Postconditions

    • token_info.patronage_rate = new_rate

BondingCurveShape

Encapsules bonding / unbonding algorithm for the AMM (namely the shape of the bonding curve). The rationale for having 2 balance types is that you might need more precision for expressing JOY in terms of CRT

pub trait BondingCurveShape<InputBalance, OutputBalance = InputBalance> {
    fn bonding_function(input_amount: InputBalance, input_reserve: InputBalance) -> Result<OutputBalance, Error>;
    fn unbonding_function(output_amount: OutputBalance, output_supply: OutputBalance) -> Result<InputBalance, Error>
}

Invariants

bonding_function(input_amount: Balance, input_reserve: Balance)

Computes the amount to return for the output currency in exchange for the input currency

  1. Preconditions

    • input_amount > input_reserve
  2. Postconditions

unbonding_function(output_amount: Balance, output_reserve: Balance)

Computes the amount to return for the output currency in exchange for the input currency

  1. Preconditions

    • output_amount > output_reserve
  2. Postconditions

IBCO

(Initial bonding curve offering) Encapsules the bonding/unbonding functionality for the liquidity curve offering. The underlying implementation idea is the following:

This is so that pricing function can be called with bonding_function(amount_to_exchange, TokenInfoById::get(token_id).supply, current_reserve(token_id)) analogous with the unbonding_function

Declaration

Implemented in pallet Module

pub trait BondingAmm<AccountId> {

    // type functions
    type OutputCurrency: MultiCurrencyBase<AccountId, TransferLocation<AccountId>>;
    type ReserveCurrency: Currency<AccountId>,
    type OutputBalance = OutputCurrency::Balance;
    type ReserveBalance = ReserveCurrency::Balance;
    type Bonding = BondingCurveShape<Self::OutputBalance, Self::ReserveBalance>;
    type TokenId = OutputCurrency::TokenId;

    // reserve functions
    fn current_reserve(token_id: Self::TokenId) -> Balance;
    fn reserve_account() -> Balance;

    // actual bonding extrinsics
    fn bond(
        origin,
        token_id: Self::TokenId,
        to_account: TransferLocation<AccountId>,
        amount: Self::OutputBalance,
    ) -> Result<Balance, Error>;

    fn unbond(
        origin,
        token_id: Self::TokenId,
        token_account: AccountId,
        amount: Self::OutputBalance,
    ) -> Result<Balance, Error>;
}

Invariants

bond(origin, token_id: TokenId, to_account: TransferLocation<AccountId>, amount: ReserveBalance)

Mints in the corresponding amount_to_mint derived using the BondingCurve::bonding_function algorithm into the signer supplied to_account, Transferring funds from the signer account into the AMM reserve account.

  1. Preconditions:

    • matches!(Ok(sender), ensure_signed(origin))
    • matches!(Ok(token_data, ensure_token_exists(token_id)))
    • PRECONDITIONS[transfer(sender, reserve_account, amount)]
  2. Postconditions:

    • POSTCONDITIONS[MultiCurrency::base_deposit(token_id, to_account, amount_to_mint)]
    • token_data AMM state reserved increased by amount
    • POSTCONDITIONS[transfer(sender, reserve_account, amount)]

unbond(origin, token_id: TokenId, from_account: AccountId, amount: OutputBalance)

Burns the corresponding amount_to_burn derived using the BondingCurve::inverse_bonding_function algorithm into the signer supplied from_account, Transferring funds from the AMM reserve account into the signer account.

  1. Preconditions:

    • matches!(Ok(sender, ensure_signed(origin)))
    • matches!(Ok(token_data, ensure_token_exists(token_id)))
    • PRECONDITIONS[MultiCurrency::base_slash(token_id, to_account, amount)]
    • PRECONDITIONS[BaseCurrency::transfer(reserve_account, sender, output_amount)] , output_amonut appropriate amount computed using the pricing rule
  2. Postconditions:

    • POSTCONDITIONS[MultiCurrency::base_slash(token_id, to_account, amount)]
    • token_data AMM state reserved decreased by amount
    • POSTCONDITIONS[BaseCurrency::transfer(reserve_account, sender, output_amount)] , output_amonut appropriate amount computed using the pricing rule

Sale

Encapsulate the logic for the initial sale of a token. The actual implementation is still a bit vague because it is still not 100% clear to me the vesting logic, but fortunately Sales is one of the latest component to implement in terms of timeline.

Declaration

Implemented in the pallet Module

pub trait Sale<AccountId, BlockNumber> {
    // types computed
    type MultiCurrency: MultiCurrencyBase<AccountId> + ReservableMultiCurrency<AccountId>;
    type TokenId = MultiCurrency::TokenId;
    type Balace = MultiCurrency::Balance;
    type AdminAuthentication: AdminAuthenticator<AccountId, Self::TokenId>;

    // types provided
    type SaleId;

    fn issue_sale(
        origin,
        token_id: Self::TokenId,
        start: BlockNumber,
        duration: BlockNumber,
        exchange_rate: Balance, // this should be a floating point
        whitelist_commitment: Hash,
    ) -> Result<(), Error>;

    fn update_sale_start(
        origin,
        sale_id: Self::SaleId,
        start: BlockNumber,
    ) -> Result<(), Error>;

    fn update_sale_duration(
        origin,
        sale_id: Self::SaleId,
        duration: BlockNumber,
    ) -> Result<(), Error>;

    fn purchase(
        origin,
        token_id: Self::TokenId,
        sale_id: Self::SaleId,
        amount: Self::Balance
    ) -> Result<(), Error>;
}

issue_sale(origin, token_id: Self::TokenId, start: BlockNumber, duration: BlockNumber, exchange_rate: Balance, whitelist_commitment: Hash, cap: Balance, locked: Balance)

Issues the sales for the token token_id with the specified parameters

  1. Preconditions

    • ensure_token_exists(token_id).is_ok())
    • AdminAuthentication::ensure_admin_authentication(origin, token_id).is_ok()
    • System::block_number() < start
    • !cap.is_zero()
  2. Postconditions

    • OLD[NextSaleId::get()] + 1 = NextSaleId::get()
    • SaleByTokenAndId::insert(token_id, sale_id, Sale{..})

update_sale_start(origin, token_id: Self::TokenId, sale_id: Self::SaleId, start: BlockNumber)

  1. Preconditions

    • ensure_token_exists(token_id).is_ok())
    • matches!(Ok(sale), ensure_sale_exists(sale_id))
    • AdminAuthentication::ensure_admin_authentication(origin, token_id).is_ok()
  2. Postconditions

    • sale.start = start

update_sale_duration(origin, sale_id: Self::SaleId, duration: BlockNumber)

  1. Preconditions

    • ensure_token_exists(token_id).is_ok()) and state is Sale
    • matches!(Ok(sale), ensure_sale_exists(sale_id))
    • AdminAuthentication::ensure_admin_authentication(origin, token_id).is_ok()
  2. Postconditions

    • sale.duration = duration

puchase(origin, token_id: TokenId, sale_id: SaleId, to_location: TransferLocation<AccountId>)

Allows a participant to purchase specified amount of token after the sale has ended

  1. Preconditions

    • matches!(Ok(sender), ensure_origin_signer(origin))
    • to_location.account() = sender
    • ensure_token_exists(token_id).is_ok() and state is Sale
    • matches!(Ok(sale), ensure_sale_exists(token_id, sale_id))
    • sales.balance - max(blocks_since_sale_ended*unlocking_rate + cliff, crt_sold)
  2. Postconditions

    • max(blocks_since_sale_ended*unlocking_rate + cliff, sale.cap) transferred to purchaser account according to ongoing policy
    • sales.balance updated accordingly

References

ignazio-bovo commented 2 years ago

Addendum:

bedeho commented 2 years ago

Overall, phenomenal effort on this, in particular on the rigorous specification of extrinsics. I think however, this become quite a bit more detailed than was needed, as the overall question that we were trying to pin down was mostly just how this pallet interfaces with the rest of the runtime:

  1. what does $CRT pallet call inside runtime
  2. what extrinsics does $CRT directly expose
  3. what does rest of the runtime get to call in $CRT pallet.

Anyway, I gave a review of some topics, I did not go into detail of all extrinsics, as I think ti will actually be easier to get synched on that through iterative implementation work. However, the fundamental questions (1-3) above seem unanswered to me still

ignazio-bovo commented 2 years ago

Extrinsic list:

The above all require account authentication (via origin parameter) and some also owner authentication via the (origin, token_id) pair. (This is what the AdminAuthenticator trait was meant to be for).

All other functions that are not listed above are meant to be library calls and they assume previous authentication. For example if someone wants to issue a token in the Content pallet then he has to:

This means that, in the example above, is the content pallet responsibility to ensure that for each channel there can be at most 1 CRT, and for each CRT token there must be exactly one underlying channel. I think that this is necessary if we want to keep the project token pallet agnostic about any other possible pallet where it's needed

The pallet itself doesn`t depend/uses on any other (Joystream) pallets. At least at this stage of the implementation process

bedeho commented 2 years ago

This was very useful, list of extrinsics was quite sensible and clear.

ignazio-bovo commented 2 years ago

I don't think we can do change_to_permissioned, as you are then effectiely opennig up the door to freezing people's assets. That is a non-starter.

So you are saying that Permissionless state is a terminal state (since we can't revert to Permissioned)?

About the token owner authentication part. What we really want to establish is, (origin, token_id) -> origin is signed by the token_id or not, approaches so far:

The above approach can be reused in any other pallet making use of the project token pallet

Addendum: I am more favorable for option 2

bedeho commented 2 years ago

So you are saying that Permissionless state is a terminal state (since we can't revert to Permissioned)?

Yes

I didn' get if the actual ensure_is_token_issuer implementation should be inside the content pallet or inside the token pallet

It could not be inside CRT pallet, because that would not make the same implementation reusable across different types of owners, then we may as well just hard code owner types directly in TokenData, it makes no difference. With what I am saying, you can take the exact same pallet code, and someone could use it in another runtime with totally diffeent ownership conscepts, or, if we want multiple instances of the same pallet (instance pallet) in our runtime, they could use different ownership representations.

So what I meant is that ensure_is_token_issuer would live in content pallet, or in just some runtime level integration code which provides implementation of trait, outside any pallet.

An alternative to that is having:

This does not work as far as I see, it cannot capture that authentication for one type of owner requires different information to another. To authenticate curator group owned channel, the id of the worker must be provided. To authetnicate a member owned channel, no extra infromation is needed at all. This is a fundamental distinction.

bedeho commented 2 years ago

Summary

So we decided that in order to avoid having to

we will go with the following:

ensure_is_token_issuer(origin, opaque_issuer_id: Vec<u8>)

where opaque_issuer_id basically will for our runtime be passed, by user via extrinsic, an Codec encoded version of the following type that does not live in the token pallet

struct TokenOperator {
 channel_id: ChannelId,
 type: TokenOperatorActor
}

enum TokenOperatorActor {
 Member,
 Curator(CuratorId)
 }

Names are horrible, I know, I'm rushed, idea is just to capture both id of channel and whomever is trying to do something as the operator of the channel, in one representation. With this, the ensure_is_token_issuer can without iteration, or extra state, fully authenticate the caller as a legitimate token operator.

bedeho commented 2 years ago

So it occurred to me: is it a good idea that $JOY account of each token lives outside of token pallet? If it is, how should project pallet interact with it? In particular when it comes to revenue splits, the initiation of a split requires that somehow the amount being paid out is not spendable, and that people cashing out their share of the split are able to transfer part of these unspendable funds during this split period. If the account lives outside of the token pallet, then the question becomes how to do this properly? At the very least issue_tokenwould require a parameter which provides the account id, or there would need to be a mapping from token id to channel in the content pallet again.

I don't see that in this design, am I missing it?

ignazio-bovo commented 2 years ago

I didn't put it in the design.

When a revenue split is initiated, it is for a certain amount X (user input) of $JOY, no more than the current balance of the channel, Y. The owner specifies some account during split initiation, where the owner share of X is diverted, that is X*(1 - S/100)

We can have something like split_accounts (internal to the modules) like we have for channel accounts and issue_split will need to know the channel_account from which a transfer X amount of JOY to the split_account. Every split claim will consists: balance_pallet::transfer(spit_account, member_account, X*S/100).

ignazio-bovo commented 2 years ago

With that we should have also an function that lives externally like: funds_account_for_admin(origin, opaque_id: Vec<u8>) -> AccountId that it is a variant of the function you suggested which retrieves the internal channel account.

bedeho commented 2 years ago

Summary

So we decided after a quick call

bedeho commented 2 years ago

Observation

Perhaps we should just make all issuer actions into non-extrinsics, so pallet methods? This would

In fact we are already assuming some of this stuff is possible by what we are doing here => https://github.com/Joystream/joystream/issues/2362#issuecomment-1060918599

Is there any downside?

bedeho commented 2 years ago

We decided to go with summary in last post.

bedeho commented 2 years ago

Close AMM

I think it should be possible to turn off AMM again, it should be possible only by the issuer and require that it has no bonded $JOY at the moment. Probably useful to allow closing it and selling sufficient $CRT to buy back all $JOY, so at to deplete AMM and satisfy this constraint in one step.

ignazio-bovo commented 2 years ago

Vesting schedule quick question

vested amount at block block for linear + cliff vesting schedule: vested_amount(block) = cliff + amount_per_block * (block - vesting_start_block) with vesting_start_block <= block <= vesting_end_block.

bedeho commented 2 years ago

Not exactly, the cliff refers to the duration from start to the point in time at which the quantity of tokens you have access to actually starts to increase, before this, nothing is happening as time passes. So this is the formula

V(b) = 0 if b < start + cliff V(b) = max{allocation, base_liquidity + (b - [start + cliff])*vesting_rate } else

where

alternatively viewed graphically like

vesting

ignazio-bovo commented 2 years ago

Is it possible to have multiple vesting schedules ongoing for a token? I.e. given token_id there can be at minimum 0 and at most n = 1 vesting schedules at any given time?

bedeho commented 2 years ago

Is it possible to have multiple vesting schedules ongoing for a token? I.e. given token_id there can be at minimum 0 and at most n = 1 vesting schedules at any given time?

I think the issuer can self-impose a particular schedule when issuing, but then during a sale a new schedule can be imposed for the new amount. This basically implies that you need to be able to have multiple distinct schedules in play on the same account (or now member), because we are allowing multiple sales over time, and also the issuer could participate both in initial issuance and a later sale, which would require at least two simultanous schedules. We can put a limit, say a max number of schedules, and then allow a member to clean out an old schedule when it no longer applies, i.e. any b where V(b) = allocation.

So basically we go from

struct AccountData {
//unlikely to just be balance,
staked_for_revenue_split: StakingType 

// include cliff block, and vesting rate, and balance schedule applies to
// **Is immutable:** does not get updated over time, only describes schedule that may have initially applied
vesting_schedule: VestingSchedule 

// includes balance that can be transferred, and balance that is encumbered by vesting schedule

not_staked_balance: Balance
}

to


struct AccountData {
//unlikely to just be balance,
staked_for_revenue_split: StakingType 

// include cliff block, and vesting rate, and balance schedule applies to
// each schedule is immutable, and can get cleanout out when expired through a dedicated extrinsic
// an length of Vec must be hard capped.
vesting_schedules: Vec<VestingSchedule>

// includes balance that can be transferred, and balance that is encumbered by vesting schedule

not_staked_balance: Balance
}
Lezek123 commented 2 years ago

While discussing it with @ignazio-bovo I was considering an introduction of a separate AccountId => Vec<VestingSchedule> map in the storage and then allow users to claim the vested tokens through a separate extrinsic in order to limit the amount of computation (including storage read, calculations etc.) done every time we need to retrieve token holder's usable balance. Do you think that would be a good approach @bedeho ?

bedeho commented 2 years ago

in order to limit the amount of computation

First off, the only computational cost that really is relevant is db-io, realistically, separating this out into a different map from the core account data would only increase that, perhaps I am missing something? There is no extra db-io to what you will have to do anyway when you are doing any sort of transfer, and thus will need to load the AccountData instance anyway. Realistically vesting_schedulescould even be really large before it would have any effect, easily many dusins of instances before the size of the storage object itself started having any weight impact.

Just to level set, the main storage representation we are using now, given that we will not use raw accounts, is TokenIdxMemberId=> AccountData.

Lezek123 commented 2 years ago

This is just the idea I've had based on the fact that similar data structures (like balances.locks or balances.reserves) are usually separate maps in Substrate. I'm actually not sure at which point it becomes an advantage to use a separate map vs large single storage object, but my hunch was that since we'll load AccountData very often and we'll need vesting_schedules only during claim, it may be more efficient to separate those.

That beeing said, if we expect the amount of vesting_schedules per member to always be very small (and we can guarantee it in some way), maybe that's a premature optimalization.

bedeho commented 2 years ago

based on the fact that similar data structures (like balances.locks or balances.reserves) are usually separate maps in Substrate.

Yes, perhaps because they are general purpose and cannot make assumptions about what sort of staking or locking people will want to use in their chain? In our case we have a specific use case, not a general fungible token system to be used for generalised staking of various kinds. If we look at our concrete locks and reservations, we also end up having 2-3 locks simultaneously at most.

very often and we'll need vesting_schedules only during claim, it may be more efficient to separate those.

Do we need a claim? our vesting constrain is basically like a lock with a temporal dimension, you don't need to do anything to claim or realize the tokens, its just an encumbrance that will decline in significance on its own.

That beeing said, if we expect the amount of vesting_schedules per member to always be very small (and we can guarantee it in some way), maybe that's a premature optimalization.

I mean in terms of number of schedules, then I think yes, at least for normal users. Like how many different simultaneous schedules do you need as one member on one token? At most 2 perhaps?