diff --git a/Cargo.lock b/Cargo.lock index 04d743e8..5306e030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8279,6 +8279,22 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-faucet" +version = "0.0.0" +dependencies = [ + "env_logger 0.11.5", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core 34.0.0", + "sp-io 38.0.0", + "sp-runtime 39.0.0", +] + [[package]] name = "pallet-grandpa" version = "37.0.0" @@ -9689,6 +9705,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collator-selection", + "pallet-faucet", "pallet-insecure-randomness-collective-flip", "pallet-market", "pallet-message-queue", diff --git a/Cargo.toml b/Cargo.toml index a80f02b0..4468bd03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "lib/polka-storage-proofs", "maat", "node", + "pallets/faucet", "pallets/market", "pallets/proofs", "pallets/randomness", @@ -127,6 +128,7 @@ zombienet-support = "=0.2.14" # Local cli-primitives = { path = "primitives/cli" } mater = { path = "storage/mater" } +pallet-faucet = { path = "pallets/faucet", default-features = false } pallet-market = { path = "pallets/market", default-features = false } pallet-proofs = { path = "pallets/proofs", default-features = false } pallet-randomness = { path = "pallets/randomness", default-features = false } diff --git a/cli/artifacts/metadata.scale b/cli/artifacts/metadata.scale index 62c06839..d67922f2 100644 Binary files a/cli/artifacts/metadata.scale and b/cli/artifacts/metadata.scale differ diff --git a/pallets/faucet/Cargo.toml b/pallets/faucet/Cargo.toml new file mode 100644 index 00000000..ab8a3ec1 --- /dev/null +++ b/pallets/faucet/Cargo.toml @@ -0,0 +1,36 @@ +[package] +authors.workspace = true +description = "exposes a drip function for getting funds on testnet" +edition.workspace = true +homepage.workspace = true +license-file.workspace = true +name = "pallet-faucet" +publish = false +repository.workspace = true +version = "0.0.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, default-features = false, features = ["derive"] } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +log = { workspace = true } +pallet-balances = { workspace = true, default-features = false } +scale-info = { workspace = true, default-features = false, features = ["derive"] } + +[dev-dependencies] +env_logger = { workspace = true } +sp-core = { workspace = true, default-features = false } +sp-io = { workspace = true } +sp-runtime = { workspace = true, default-features = false } + +[features] +default = ["std"] +runtime-benchmarks = ["frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks"] +std = ["codec/std", "frame-support/std", "frame-system/std", "pallet-balances/std", "scale-info/std", "sp-core/std", "sp-io/std", "sp-runtime/std"] +try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime", "sp-runtime/try-runtime"] diff --git a/pallets/faucet/src/lib.rs b/pallets/faucet/src/lib.rs new file mode 100644 index 00000000..e3ffaef5 --- /dev/null +++ b/pallets/faucet/src/lib.rs @@ -0,0 +1,110 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod test; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use frame_support::{ + pallet_prelude::*, + traits::{Currency, ReservableCurrency}, + }; + use frame_system::{ensure_none, pallet_prelude::*}; + + /// Allows to extract Balance of an account via the Config::Currency associated type. + /// BalanceOf is a sophisticated way of getting an u128. + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The currency mechanism. + type Currency: ReservableCurrency; + + /// The amount that is dispensed in planck's + #[pallet::constant] + type FaucetDripAmount: Get>; + + /// How often an account can use the drip function (1 day on testnet) + #[pallet::constant] + type FaucetDripDelay: Get>; + } + + /// By default pallet do no allow for unsigned transactions. + /// Implementing this trait for the faucet Pallet allows unsigned extrinsics to be called. + /// There is no complicated implementation needed (like checking the call type) + /// because there is only one transaction in this pallet + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned( + _source: TransactionSource, + _call: &Self::Call, + ) -> TransactionValidity { + let current_block = >::block_number(); + ValidTransaction::with_tag_prefix("pallet-faucet") + .and_provides(current_block) + .build() + } + } + + /// Keeps track of when accounts last used the drip function. + #[pallet::storage] + #[pallet::getter(fn drips)] + pub type Drips = StorageMap<_, _, T::AccountId, BlockNumberFor>; + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + /// Emitted when an account uses the drip function successfully. + Dripped { + who: T::AccountId, + when: BlockNumberFor, + }, + } + + #[pallet::error] + pub enum Error { + /// Emitted when an account tries to call the drip function more than 1x in 24 hours. + FaucetUsedRecently, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight((Weight::zero(), Pays::No))] + pub fn drip(origin: OriginFor, account: T::AccountId) -> DispatchResult { + ensure_none(origin)?; + let current_block = >::block_number(); + if let Some(faucet_block) = Self::drips(&account) { + ensure!( + current_block >= (faucet_block + T::FaucetDripDelay::get()), + { + log::error!("{account:?} has recently used the faucet"); + Error::::FaucetUsedRecently + } + ); + } + log::info!("Dripping {:?} to {account:?}", T::FaucetDripAmount::get()); + // Infallible https://docs.rs/frame-support/latest/frame_support/traits/tokens/currency/trait.Currency.html#tymethod.issue + let imbalance = T::Currency::issue(T::FaucetDripAmount::get()); + T::Currency::resolve_creating(&account, imbalance); + Drips::::insert(account.clone(), current_block); + Self::deposit_event(Event::::Dripped { + who: account, + when: current_block, + }); + Ok(()) + } + } +} diff --git a/pallets/faucet/src/mock.rs b/pallets/faucet/src/mock.rs new file mode 100644 index 00000000..641fbb45 --- /dev/null +++ b/pallets/faucet/src/mock.rs @@ -0,0 +1,97 @@ +use frame_support::{derive_impl, parameter_types, traits::Hooks}; +use frame_system::{self as system}; +use sp_core::Pair; +use sp_runtime::{ + traits::{IdentifyAccount, IdentityLookup, Verify}, + AccountId32, BuildStorage, MultiSignature, MultiSigner, +}; + +use crate::{self as pallet_faucet, BalanceOf}; + +pub const ALICE: &'static str = "//Alice"; + +type Block = frame_system::mocking::MockBlock; +type BlockNumber = u64; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Faucet: pallet_faucet, + } +); + +pub type Signature = MultiSignature; +pub type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +parameter_types! { + pub const FaucetDripAmount: BalanceOf = 10_000_000_000_000; + pub const FaucetDripDelay: BlockNumber = 1; +} + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type FaucetDripAmount = FaucetDripAmount; + type FaucetDripDelay = FaucetDripDelay; +} + +pub fn key_pair(name: &str) -> sp_core::sr25519::Pair { + sp_core::sr25519::Pair::from_string(name, None).unwrap() +} + +pub fn account(name: &str) -> AccountId32 { + let user_pair = key_pair(name); + let signer = MultiSigner::Sr25519(user_pair.public()); + signer.into_account() +} + +pub fn events() -> Vec { + let evt = System::events() + .into_iter() + .map(|evt| evt.event) + .collect::>(); + System::reset_events(); + evt +} + +/// Run until a particular block. +/// +/// Stolen't from: +pub fn run_to_block(n: u64) { + while System::block_number() < n { + if System::block_number() > 1 { + System::on_finalize(System::block_number()); + } + + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + } +} + +/// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let t = system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/faucet/src/test.rs b/pallets/faucet/src/test.rs new file mode 100644 index 00000000..2bd710f2 --- /dev/null +++ b/pallets/faucet/src/test.rs @@ -0,0 +1,72 @@ +use frame_support::{assert_err, assert_ok}; +use frame_system::Event as SystemEvent; +use pallet_balances::Event as BalanceEvent; + +use crate::{mock::*, Error, Event}; + +#[test] +fn drip() { + new_test_ext().execute_with(|| { + let account = account::(ALICE); + assert_ok!(Faucet::drip(RuntimeOrigin::none(), account.clone())); + + // The initial drip should create the account + assert_eq!( + events(), + [ + RuntimeEvent::Balances(BalanceEvent::Deposit { + who: account.clone(), + amount: ::FaucetDripAmount::get() + }), + RuntimeEvent::System(SystemEvent::NewAccount { + account: account.clone() + }), + RuntimeEvent::Balances(BalanceEvent::Endowed { + account: account.clone(), + free_balance: ::FaucetDripAmount::get() + }), + RuntimeEvent::Faucet(Event::Dripped { + who: account.clone(), + when: System::block_number() + }) + ] + ); + + assert_eq!( + Balances::free_balance(account.clone()), + ::FaucetDripAmount::get() + ); + + // Check that dripping at the same block is blocked + assert_err!( + Faucet::drip(RuntimeOrigin::none(), account.clone()), + Error::::FaucetUsedRecently + ); + + // Run to block_number + faucet_delay + run_to_block(System::block_number() + ::FaucetDripDelay::get()); + + // Rerun drip, should be successful + assert_ok!(Faucet::drip(RuntimeOrigin::none(), account.clone())); + + // Expecting less events because no new account is created + assert_eq!( + events(), + [ + RuntimeEvent::Balances(BalanceEvent::Deposit { + who: account.clone(), + amount: ::FaucetDripAmount::get() + }), + RuntimeEvent::Faucet(Event::Dripped { + who: account.clone(), + when: System::block_number() + }) + ] + ); + + assert_eq!( + Balances::free_balance(account), + ::FaucetDripAmount::get() * 2 + ); + }); +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d42d1674..869fff92 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -20,6 +20,7 @@ substrate-wasm-builder = { workspace = true, optional = true } [dependencies] # Local Pallets +pallet-faucet = { workspace = true, default-features = false } pallet-market = { workspace = true, default-features = false } pallet-proofs = { workspace = true, default-features = false } pallet-randomness = { workspace = true, default-features = false } diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 727d5794..55694e3c 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -368,6 +368,10 @@ parameter_types! { // Randomness pallet pub const CleanupInterval: BlockNumber = DAYS; pub const SeedAgeLimit: BlockNumber = 30 * DAYS; + + // Faucet pallet + pub const FaucetDripAmount: Balance = 10_000_000_000_000; + pub const FaucetDripDelay: BlockNumber = DAYS; } impl pallet_storage_provider::Config for Runtime { @@ -418,6 +422,14 @@ impl pallet_proofs::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +#[cfg(feature = "testnet")] +impl pallet_faucet::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type FaucetDripAmount = FaucetDripAmount; + type FaucetDripDelay = FaucetDripDelay; +} + /// Config for insecure randomness impl pallet_insecure_randomness_collective_flip::Config for Runtime {} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c3bc7c71..d9685d17 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -238,6 +238,7 @@ pub fn native_version() -> NativeVersion { } // Create the runtime by composing the FRAME pallets that were previously configured. +#[cfg(not(feature = "testnet"))] #[frame_support::runtime] mod runtime { #[runtime::runtime] @@ -308,6 +309,80 @@ mod runtime { pub type Randomness = pallet_randomness; } +// Create the runtime by composing the FRAME pallets that were previously configured. +#[cfg(feature = "testnet")] +#[frame_support::runtime] +mod runtime { + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask + )] + pub struct Runtime; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + #[runtime::pallet_index(1)] + pub type ParachainSystem = cumulus_pallet_parachain_system; + #[runtime::pallet_index(2)] + pub type Timestamp = pallet_timestamp; + #[runtime::pallet_index(3)] + pub type ParachainInfo = parachain_info; + // Temporary. Will be removed after we switch to babe + #[runtime::pallet_index(4)] + pub type RandomnessSource = pallet_insecure_randomness_collective_flip; + + // Monetary stuff. + #[runtime::pallet_index(10)] + pub type Balances = pallet_balances; + #[runtime::pallet_index(11)] + pub type TransactionPayment = pallet_transaction_payment; + + // Governance + #[runtime::pallet_index(15)] + pub type Sudo = pallet_sudo; + + // Collator support. The order of these 4 are important and shall not change. + #[runtime::pallet_index(20)] + pub type Authorship = pallet_authorship; + #[runtime::pallet_index(21)] + pub type CollatorSelection = pallet_collator_selection; + #[runtime::pallet_index(22)] + pub type Session = pallet_session; + #[runtime::pallet_index(23)] + pub type Aura = pallet_aura; + #[runtime::pallet_index(24)] + pub type AuraExt = cumulus_pallet_aura_ext; + + // XCM helpers. + #[runtime::pallet_index(30)] + pub type XcmpQueue = cumulus_pallet_xcmp_queue; + #[runtime::pallet_index(31)] + pub type PolkadotXcm = pallet_xcm; + #[runtime::pallet_index(32)] + pub type CumulusXcm = cumulus_pallet_xcm; + #[runtime::pallet_index(33)] + pub type MessageQueue = pallet_message_queue; + + #[runtime::pallet_index(34)] + pub type StorageProvider = pallet_storage_provider::pallet; + #[runtime::pallet_index(35)] + pub type Market = pallet_market; + #[runtime::pallet_index(36)] + pub type Proofs = pallet_proofs; + #[runtime::pallet_index(37)] + pub type Randomness = pallet_randomness; + #[runtime::pallet_index(38)] + pub type Faucet = pallet_faucet; +} + #[docify::export(register_validate_block)] cumulus_pallet_parachain_system::register_validate_block! { Runtime = Runtime,