JoshOrndorff / recipes

A Hands-On Cookbook for Aspiring Blockchain Chefs
GNU General Public License v3.0
378 stars 186 forks source link

Recipes for FRAME v2 simple_crowdloan example #451

Closed Chralt98 closed 2 years ago

Chralt98 commented 2 years ago

Hey, it would be nice to see the examples with the newer version of FRAME (v2). Is something in sight?

JoshOrndorff commented 2 years ago

@noctrlz updated one of them https://github.com/substrate-developer-hub/recipes/pull/450

To be honest, picking a simple one and updating it would be a great way to learn.

Chralt98 commented 2 years ago

@JoshOrndorff thank you for your quick reply. I am facing the issue to translate a more complex one (simple_crowdfund).

But I get an error for inserting objects to the Funds structure. And under impl Pallet there is the child not found.

#![cfg_attr(not(feature = "std"), no_std)]

/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://substrate.dev/docs/en/knowledgebase/runtime/frame>
use frame_support::{
    pallet_prelude::*,
    traits::{Currency, ExistenceRequirement, ReservableCurrency, WithdrawReasons},
};
use sp_runtime::traits::{Saturating, Zero};

type BalanceOf<T> =
    <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

// EXPLA: additional struct for information about each fund which is stored
#[derive(Encode, Decode, Default, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct FundInfo<AccountId, Balance, BlockNumber> {
    /// The account that will recieve the funds if the campaign is successful
    beneficiary: AccountId,
    /// The amount of deposit placed
    deposit: Balance,
    /// The total amount raised
    raised: Balance,
    /// Block number after which funding must have succeeded
    end: BlockNumber,
    /// Upper bound on `raised`
    goal: Balance,
}

type AccountIdOf<T> = <T as frame_system::Config>::AccountId;

type FundInfoOf<T> =
    FundInfo<AccountIdOf<T>, BalanceOf<T>, <T as frame_system::Config>::BlockNumber>;

pub use pallet::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

#[frame_support::pallet]
pub mod pallet {
    use super::*;
    use frame_system::pallet_prelude::*;

    // EXPLA: number how many funds have been ever created
    /// Simple index for identifying a fund.
    pub type FundIndex = u32;

    /// Configure the pallet by specifying the parameters and types on which it depends.
    #[pallet::config]
    pub trait Config: frame_system::Config {
        /// Because this pallet emits events, it depends on the runtime's definition of an event.
        type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

        /// The currency in which the crowdfunds will be denominated
        type Currency: ReservableCurrency<Self::AccountId>;

        /// The amount to be held on deposit by the owner of a crowdfund
        #[pallet::constant]
        type SubmissionDeposit: Get<BalanceOf<Self>>;

        /// The minimum amount that may be contributed into a crowdfund. Should almost certainly be at
        /// least ExistentialDeposit.
        #[pallet::constant]
        type MinContribution: Get<BalanceOf<Self>>;

        /// The period of time (in blocks) after an unsuccessful crowdfund ending during which
        /// contributors are able to withdraw their funds. After this period, their funds are lost.
        #[pallet::constant]
        type RetirementPeriod: Get<Self::BlockNumber>;
    }

    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    #[pallet::storage]
    #[pallet::getter(fn funds)]
    pub(super) type Funds<T> = StorageMap<_, Blake2_128Concat, FundIndex, Option<FundInfoOf<T>>, ValueQuery>;

    #[pallet::storage]
    #[pallet::getter(fn fund_count)]
    pub(super) type FundCount<T> = StorageValue<_, FundIndex, ValueQuery>;

    // EXPLA: storage additional comment
    // EXPLA: child trie (information about how many funds been contributed) this is not explicity declared anywhere
    // EXPLA: query the child trie via the Child Trie API
    // Additional information is stored i na child trie. See the helper
    // functions in the impl<T: Config> Pallet<T> block below

    // TODO: remove this storage item later
    // The pallet's runtime storage items.
    // https://substrate.dev/docs/en/knowledgebase/runtime/storage
    #[pallet::storage]
    #[pallet::getter(fn something)]
    // Learn more about declaring storage items:
    // https://substrate.dev/docs/en/knowledgebase/runtime/storage#declaring-storage-items
    pub type Something<T> = StorageValue<_, u32>;

    // Pallets use events to inform users when important changes are made.
    // https://substrate.dev/docs/en/knowledgebase/runtime/events
    #[pallet::event]
    #[pallet::metadata(T::AccountId = "AccountId", T::BlockNumber = "BlockNumber", BalanceOf<T> = "Balance")]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        /// Event documentation should end with an array that provides descriptive names for event
        /// parameters. [something, who]
        SomethingStored(u32, T::AccountId),
        Created(FundIndex, T::BlockNumber),
        Contributed(T::AccountId, FundIndex, BalanceOf<T>, T::BlockNumber),
        Withdrew(T::AccountId, FundIndex, BalanceOf<T>, T::BlockNumber),
        Retiring(FundIndex, T::BlockNumber),
        Dissolved(FundIndex, T::BlockNumber, T::AccountId),
        Dispensed(FundIndex, T::BlockNumber, T::AccountId),
    }

    // Errors inform users that something went wrong.
    #[pallet::error]
    pub enum Error<T> {
        /// Crowdfund must end after it starts
        EndTooEarly,
        /// Must contribute at least the minimum amount of funds
        ContributionTooSmall,
        /// The fund index specified does not exist
        InvalidIndex,
        /// The crowdfund's contribution period has ended; no more contributions will be accepted
        ContributionPeriodOver,
        /// You may not withdraw or dispense funds while the fund is still active
        FundStillActive,
        /// You cannot withdraw funds because you have not contributed any
        NoContribution,
        /// You cannot dissolve a fund that has not yet completed its retirement period
        FundNotRetired,
        /// Cannot dispense funds from an unsuccessful fund
        UnsuccessfulFund,
    }

    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}

    // EXPLA: Child Trie API
    // EXPLA: this pallet uses one trie for each active crowdfund => need to generate an unique child info for each crowdfund (look pub fn id_from_index(index: FundIndex) -> child::ChildInfo below)
    // Dispatchable functions allows users to interact with the pallet and invoke state changes.
    // These functions materialize as "extrinsics", which are often compared to transactions.
    // Dispatchable functions must be annotated with a weight and must return a DispatchResult.
    #[pallet::call]
    impl<T: Config> Pallet<T> {
        /// Create a new fund
        #[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
        pub fn create(
            origin: OriginFor<T>,
            beneficiary: AccountIdOf<T>,
            goal: BalanceOf<T>,
            end: T::BlockNumber,
        ) -> DispatchResult {
            let creator = ensure_signed(origin)?;
            let now = <frame_system::Pallet<T>>::block_number();

            ensure!(end > now, Error::<T>::EndTooEarly);

            let deposit = T::SubmissionDeposit::get();
            let imb = T::Currency::withdraw(
                &creator,
                deposit,
                WithdrawReasons::TRANSFER,
                ExistenceRequirement::AllowDeath,
            )?;

            let index = FundCount::get();
            // not protected against overflow, see safemath section
            FundCount::put(index + 1);

            // No fees are paid here if we need to create this account; that's why we don't just
            // use the stock `transfer`.
            T::Currency::resolve_creating(&Self::fund_account_id(index), imb);

            <Funds<T>>::insert(
                index,
                FundInfo {
                    beneficiary,
                    deposit,
                    raised: Zero::zero(),
                    end,
                    goal,
                },
            );

            Self::deposit_event(Event::Created(index, now));
            // Return a successful DispatchResultWithPostInfo
            Ok(())
        }

        /// Contribute funds to an existing fund
        #[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
        pub fn contribute(
            origin: OriginFor<T>,
            index: FundIndex,
            value: BalanceOf<T>,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;

            ensure!(
                value >= T::MinContribution::get(),
                Error::<T>::ContributionTooSmall
            );
            let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidIndex)?;

            // Make sure crowdfund has not ended
            let now = <frame_system::Pallet<T>>::block_number();
            ensure!(fund.end > now, Error::<T>::ContributionPeriodOver);

            // Add contribution to the fund
            T::Currency::transfer(
                &who,
                &Self::fund_account_id(index),
                value,
                ExistenceRequirement::AllowDeath,
            )?;
            fund.raised += value;
            Funds::<T>::insert(index, &fund);

            let balance = Self::contribution_get(index, &who);
            let balance = balance.saturating_add(value);
            Self::contribution_put(index, &who, &balance);

            Self::deposit_event(Event::Contributed(who, index, balance, now));
            Ok(())
        }

        /// Withdraw full balance of a contributor to a fund
        #[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))]
        pub fn withdraw(
            origin: OriginFor<T>,
            #[pallet::compact] index: FundIndex,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;

            let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidIndex)?;
            let now = <frame_system::Pallet<T>>::block_number();
            ensure!(fund.end < now, Error::<T>::FundStillActive);

            let balance = Self::contribution_get(index, &who);
            ensure!(balance > Zero::zero(), Error::<T>::NoContribution);

            // Return funds to caller without charging a transfer fee
            let _ = T::Currency::resolve_into_existing(
                &who,
                T::Currency::withdraw(
                    &Self::fund_account_id(index),
                    balance,
                    WithdrawReasons::TRANSFER,
                    ExistenceRequirement::AllowDeath,
                )?,
            );

            // Update storage
            Self::contribution_kill(index, &who);
            fund.raised = fund.raised.saturating_sub(balance);
            <Funds<T>>::insert(index, &fund);

            Self::deposit_event(Event::Withdrew(who, index, balance, now));
            Ok(())
        }

        /// Dissolve an entire crowdfund after its retirement period has expired.
        /// Anyone can call this function, and they are incentivized to do so because
        /// they inherit the deposit.
        #[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
        pub fn dissolve(origin: OriginFor<T>, index: FundIndex) -> DispatchResult {
            let reporter = ensure_signed(origin)?;

            let fund = Self::funds(index).ok_or(Error::<T>::InvalidIndex)?;

            // Check that enough time has passed to remove from storage
            let now = <frame_system::Pallet<T>>::block_number();
            ensure!(
                now >= fund.end + T::RetirementPeriod::get(),
                Error::<T>::FundNotRetired
            );

            let account = Self::fund_account_id(index);

            // Dissolver collects the deposit and any remaining funds
            let _ = T::Currency::resolve_creating(
                &reporter,
                T::Currency::withdraw(
                    &account,
                    fund.deposit + fund.raised,
                    WithdrawReasons::TRANSFER,
                    ExistenceRequirement::AllowDeath,
                )?,
            );

            // Remove the fund info from storage
            <Funds<T>>::remove(index);
            // Remove all the contributor info from storage in a single write.
            // This is possible thanks to the use of a child tree.
            Self::crowdfund_kill(index);

            Self::deposit_event(Event::Dissolved(index, now, reporter));
            Ok(())
        }

        // EXPLA: final reward spending of the crowdfund
        /// Dispense a payment to the beneficiary of a successful crowdfund.
        /// The beneficiary receives the contributed funds and the caller receives
        /// the deposit as a reward to incentivize clearing settled crowdfunds out of storage.
        #[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
        pub fn dispense(origin: OriginFor<T>, index: FundIndex) -> DispatchResult {
            let caller = ensure_signed(origin)?;

            let fund = Self::funds(index).ok_or(Error::<T>::InvalidIndex)?;

            // Check that enough time has passed to remove from storage
            let now = <frame_system::Pallet<T>>::block_number();

            ensure!(now >= fund.end, Error::<T>::FundStillActive);

            // Check that the fund was actually successful
            ensure!(fund.raised >= fund.goal, Error::<T>::UnsuccessfulFund);

            let account = Self::fund_account_id(index);

            // Beneficiary collects the contributed funds
            let _ = T::Currency::resolve_creating(
                &fund.beneficiary,
                T::Currency::withdraw(
                    &account,
                    fund.raised,
                    WithdrawReasons::TRANSFER,
                    ExistenceRequirement::AllowDeath,
                )?,
            );

            // EXPLA: Data from finished funds takes up space on chain, so it is best to settle the fund and cleanup the data as soon as possible.
            // EXPLA: that is why the caller gets a deposit (so the first caller is incentivised to clear the data)
            // Caller collects the deposit
            let _ = T::Currency::resolve_creating(
                &caller,
                T::Currency::withdraw(
                    &account,
                    fund.deposit,
                    WithdrawReasons::TRANSFER,
                    ExistenceRequirement::AllowDeath,
                )?,
            );

            // Remove the fund info from storage
            <Funds<T>>::remove(index);
            // Remove all the contributor info from storage in a single write.
            // This is possible thanks to the use of a child tree.
            Self::crowdfund_kill(index);

            Self::deposit_event(Event::Dispensed(index, now, caller));
            Ok(())
        }

        /*
        read storage example

        /// An example dispatchable that may throw a custom error.
        #[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))]
        pub fn cause_error(origin: OriginFor<T>) -> DispatchResult {
            let _who = ensure_signed(origin)?;

            // Read a value from storage.
            match <Something<T>>::get() {
                // Return an error if the value has not been set.
                None => Err(Error::<T>::NoneValue)?,
                Some(old) => {
                    // Increment the value read from storage; will error in the event of overflow.
                    let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?;
                    // Update the value in storage with the incremented result.
                    <Something<T>>::put(new);
                    Ok(())
                }
            }
        }
        */
    }
}

// EXPLA: Child Trie API
// EXPLA: this pallet uses one trie for each active crowdfund => need to generate an unique child info for each crowdfund (look pub fn id_from_index(index: FundIndex) -> child::ChildInfo below)
impl<T: Config> Pallet<T> {
    /// The account ID of the fund pot.
    ///
    /// This actually does computation. If you need to keep using it, then make sure you cache the
    /// value and only call this once.
    pub fn fund_account_id(index: FundIndex) -> T::AccountId {
        PALLET_ID.into_sub_account(index)
    }

    /// Find the ID associated with the fund
    ///
    /// Each fund stores information about its contributors and their contributions in a child trie
    /// This helper function calculates the id of the associated child trie.
    pub fn id_from_index(index: FundIndex) -> child::ChildInfo {
        let mut buf = Vec::new();
        buf.extend_from_slice(b"crowdfnd");
        buf.extend_from_slice(&index.to_le_bytes()[..]);

        child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
    }

    /// Record a contribution in the associated child trie.
    pub fn contribution_put(index: FundIndex, who: &T::AccountId, balance: &BalanceOf<T>) {
        let id = Self::id_from_index(index);
        who.using_encoded(|b| child::put(&id, b, &balance));
    }

    /// Lookup a contribution in the associated child trie.
    pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> BalanceOf<T> {
        let id = Self::id_from_index(index);
        who.using_encoded(|b| child::get_or_default::<BalanceOf<T>>(&id, b))
    }

    /// Remove a contribution from an associated child trie.
    pub fn contribution_kill(index: FundIndex, who: &T::AccountId) {
        let id = Self::id_from_index(index);
        who.using_encoded(|b| child::kill(&id, b));
    }

    /// Remove the entire record of contributions in the associated child trie in a single
    /// storage write.
    pub fn crowdfund_kill(index: FundIndex) {
        let id = Self::id_from_index(index);
        // The None here means we aren't setting a limit to how many keys to delete.
        // Limiting can be useful, but is beyond the scope of this recipe. For more info, see
        // https://crates.parity.io/frame_support/storage/child/fn.kill_storage.html
        child::kill_storage(&id, None);
    }
}
Chralt98 commented 2 years ago

The next problem is, that Rust cannot find struct, variant or union type GenesisConfig in crate pallet_balances.

pub fn new_test_ext() -> sp_io::TestExternalities {
    let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
    pallet_balances::GenesisConfig::<Test> {
        balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
    }.assimilate_storage(&mut t).unwrap();
    t.into()
}
Chralt98 commented 2 years ago

Got it under https://github.com/substrate-developer-hub/substrate-how-to-guides/blob/main/example-code/template-node/pallets/simple-crowdfund/src/lib.rs Thank you!