diff --git a/Cargo.lock b/Cargo.lock index e1831c353..36a00b18c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,6 +4489,7 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-assets", "pallet-contracts", "pallet-llm", "parity-scale-codec", diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 53abf498d..02abaad55 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -349,6 +349,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "asset-timelock" +version = "0.1.0" +dependencies = [ + "ink", + "ink_e2e", + "liberland-extension", +] + [[package]] name = "async-channel" version = "2.3.1" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 9cece4eb8..945b46c6b 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,5 +1,6 @@ [workspace] resolver = "2" members = [ - "msig_court" + "asset-timelock", + "msig_court", ] \ No newline at end of file diff --git a/contracts/asset-timelock/.gitignore b/contracts/asset-timelock/.gitignore new file mode 100755 index 000000000..8de8f877e --- /dev/null +++ b/contracts/asset-timelock/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/contracts/asset-timelock/Cargo.toml b/contracts/asset-timelock/Cargo.toml new file mode 100644 index 000000000..62f72f66e --- /dev/null +++ b/contracts/asset-timelock/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "asset-timelock" +version = "0.1.0" +authors = ["[your_name] <[your_email]>"] +edition = "2021" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +liberland-extension = { path = "../../liberland-extension/ink", default-features = false} + +[dev-dependencies] +ink_e2e = { version = "5.0.0" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "liberland-extension/std", +] +ink-as-dependency = [] +e2e-tests = [] \ No newline at end of file diff --git a/contracts/asset-timelock/lib.rs b/contracts/asset-timelock/lib.rs new file mode 100644 index 000000000..0f760ac53 --- /dev/null +++ b/contracts/asset-timelock/lib.rs @@ -0,0 +1,130 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[cfg(test)] +mod mock; + +#[ink::contract(env = liberland_extension::LiberlandEnvironment)] +mod asset_timelock { + use ink::storage::Mapping; + use liberland_extension::{AssetId, AssetsBalance}; + + pub type DepositId = u32; + + #[derive(Debug, PartialEq, Eq, Clone)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + #[cfg_attr(feature = "std", derive(ink::storage::traits::StorageLayout))] + pub struct Deposit { + timestamp: Timestamp, + asset_id: AssetId, + recipient: AccountId, + amount: AssetsBalance, + } + + #[derive(Debug, PartialEq, Eq, Clone)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum Error { + /// Funds not unlocked yet + TooEarly, + /// No such deposit found + NotFound, + /// Runtime call failed + CallFailed, + /// Arithmetic overflow + Overflow, + /// Unlock timestamp is before current timestamp + TimestampFromThePast, + } + + impl From for Error { + fn from(_: liberland_extension::Error) -> Self { + Self::CallFailed + } + } + + pub type Result = core::result::Result; + + #[ink(storage)] + #[derive(Default)] + pub struct AssetTimelock { + next_deposit_id: DepositId, + deposits: Mapping, + } + + #[ink(event)] + pub struct Deposited { + #[ink(topic)] + depositor: AccountId, + #[ink(topic)] + recipient: AccountId, + deposit_id: DepositId, + asset_id: AssetId, + timestamp: Timestamp, + amount: AssetsBalance, + } + + #[ink(event)] + pub struct Withdrawn { + #[ink(topic)] + recipient: AccountId, + deposit_id: DepositId, + asset_id: AssetId, + amount: AssetsBalance, + } + + impl AssetTimelock { + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn get_deposit(&self, deposit_id: DepositId) -> Option { + self.deposits.get(deposit_id) + } + + #[ink(message)] + pub fn deposit( + &mut self, + asset_id: AssetId, + recipient: AccountId, + amount: AssetsBalance, + timestamp: Timestamp, + ) -> Result { + if timestamp < self.env().block_timestamp() { + return Err(Error::TimestampFromThePast); + } + let depositor = self.env().caller(); + let contract_account = self.env().account_id(); + let deposit_id = self.next_deposit_id; + self.next_deposit_id = self.next_deposit_id.checked_add(1).ok_or(Error::Overflow)?; + self.env().extension().asset_transfer_approved( + asset_id, + depositor, + contract_account, + amount, + )?; + self.deposits + .insert(deposit_id, &Deposit { timestamp, asset_id, recipient, amount }); + self.env().emit_event(Deposited { + depositor, + deposit_id, + asset_id, + recipient, + timestamp, + amount, + }); + Ok(deposit_id) + } + + #[ink(message)] + pub fn withdraw(&mut self, deposit_id: DepositId) -> Result<()> { + let Deposit { asset_id, recipient, amount, timestamp } = + self.deposits.take(&deposit_id).ok_or(Error::NotFound)?; + if timestamp > self.env().block_timestamp() { + return Err(Error::TooEarly); + } + self.env().extension().asset_transfer(asset_id, recipient, amount)?; + Ok(()) + } + } +} diff --git a/contracts/asset-timelock/mock.rs b/contracts/asset-timelock/mock.rs new file mode 100644 index 000000000..4642281ad --- /dev/null +++ b/contracts/asset-timelock/mock.rs @@ -0,0 +1,21 @@ +pub struct MockedLiberlandExtensionSuccess; +impl ink::env::test::ChainExtension for MockedLiberlandExtensionSuccess { + fn ext_id(&self) -> u16 { + 0 + } + + fn call(&mut self, _func_id: u16, _input: &[u8], _output: &mut Vec) -> u32 { + 0 + } +} + +pub struct MockedLiberlandExtensionFail; +impl ink::env::test::ChainExtension for MockedLiberlandExtensionFail { + fn ext_id(&self) -> u16 { + 0 + } + + fn call(&mut self, _func_id: u16, _input: &[u8], _output: &mut Vec) -> u32 { + 1 + } +} diff --git a/liberland-extension/ink/src/lib.rs b/liberland-extension/ink/src/lib.rs index 3de87919e..08bca8099 100644 --- a/liberland-extension/ink/src/lib.rs +++ b/liberland-extension/ink/src/lib.rs @@ -11,6 +11,30 @@ pub trait Liberland { #[ink(function = 1)] fn llm_force_transfer(args: LLMForceTransferArguments); + + #[ink(function = 2)] + fn asset_balance_of(asset_id: AssetId, account: AccountId) -> AssetsBalance; + + #[ink(function = 3)] + fn asset_total_supply_of(asset_id: AssetId) -> AssetsBalance; + + #[ink(function = 4)] + fn asset_approve_transfer(asset_id: AssetId, delegate: AccountId, amount: Balance); + + #[ink(function = 5)] + fn asset_cancel_approval(asset_id: AssetId, delegate: AccountId); + + #[ink(function = 6)] + fn asset_transfer(asset_id: AssetId, target: AccountId, amount: Balance); + #[ink(function = 7)] + fn asset_transfer_approved( + asset_id: AssetId, + owner: AccountId, + destination: AccountId, + amount: Balance, + ); + #[ink(function = 8)] + fn asset_transfer_keep_alive(asset_id: AssetId, target: AccountId, amount: Balance); } impl ink::env::chain_extension::FromStatusCode for Error { diff --git a/liberland-extension/ink/src/types.rs b/liberland-extension/ink/src/types.rs index 49255d55c..866e995f4 100644 --- a/liberland-extension/ink/src/types.rs +++ b/liberland-extension/ink/src/types.rs @@ -1,7 +1,9 @@ use ink::env::Environment; -type AccountId = ::AccountId; -type Balance = ::Balance; +pub type AccountId = ::AccountId; +pub type Balance = ::Balance; +pub type AssetsBalance = u128; +pub type AssetId = u32; #[derive(Debug, Clone, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] diff --git a/liberland-extension/runtime/Cargo.toml b/liberland-extension/runtime/Cargo.toml index 2e2d7f803..9f9a8d0cb 100644 --- a/liberland-extension/runtime/Cargo.toml +++ b/liberland-extension/runtime/Cargo.toml @@ -11,25 +11,26 @@ publish = false [dependencies] codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false } frame-support = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } -pallet-contracts = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } -sp-std = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } -sp-core = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } -sp-runtime = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } frame-system = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } log = { version = "0.4.17", default-features = false } - +pallet-assets = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } +pallet-contracts = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } pallet-llm = { default-features = false, path = "../../substrate/frame/llm" } +sp-core = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } +sp-runtime = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } +sp-std = { default-features = false, tag = "polkadot-v1.1.0", git = "https://github.com/paritytech/polkadot-sdk" } [features] default = ["std"] std = [ "codec/std", "frame-support/std", - "pallet-contracts/std", - "sp-std/std", - "sp-core/std", - "sp-runtime/std", "frame-system/std", "log/std", + "pallet-assets/std", + "pallet-contracts/std", "pallet-llm/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", ] \ No newline at end of file diff --git a/liberland-extension/runtime/src/lib.rs b/liberland-extension/runtime/src/lib.rs index ed2b22400..ab3251a79 100644 --- a/liberland-extension/runtime/src/lib.rs +++ b/liberland-extension/runtime/src/lib.rs @@ -3,7 +3,7 @@ use codec::{Decode, Encode, MaxEncodedLen}; use log::{error, trace}; use pallet_contracts::chain_extension::{ChainExtension, Environment, Ext, InitState, RetVal}; -use sp_runtime::DispatchError; +use sp_runtime::{traits::StaticLookup, DispatchError}; #[derive(Decode, Encode, MaxEncodedLen)] pub struct LLMForceTransferArguments { @@ -17,12 +17,9 @@ pub struct LLMForceTransferArguments { pub struct LiberlandExtension; impl LiberlandExtension { - fn llm_force_transfer( - &mut self, - env: Environment, - ) -> Result + fn llm_force_transfer(env: Environment) -> Result where - E::T: pallet_llm::Config + pallet_contracts::Config, + E::T: pallet_llm::Config, ::RuntimeCall: From>, { trace!( @@ -42,21 +39,227 @@ impl LiberlandExtension { ext.call_runtime(call).map_err(|e| e.error)?; Ok(RetVal::Converging(0)) } + + fn asset_balance_of(env: Environment) -> Result + where + E::T: pallet_assets::Config, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_balance_of" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, account): ( + ::AssetId, + ::AccountId, + ) = env.read_as()?; + + let balance = pallet_assets::Pallet::::balance(asset_id, account); + let balance_encoded = balance.encode(); + env.write(&balance_encoded, false, None)?; + Ok(RetVal::Converging(0)) + } + + fn asset_total_supply_of( + env: Environment, + ) -> Result + where + E::T: pallet_assets::Config, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_total_supply_of" + ); + + let mut env = env.buf_in_buf_out(); + let asset_id: ::AssetId = env.read_as()?; + + let supply = pallet_assets::Pallet::::total_supply(asset_id); + let supply_encoded = supply.encode(); + env.write(&supply_encoded, false, None)?; + Ok(RetVal::Converging(0)) + } + + fn asset_approve_transfer( + env: Environment, + ) -> Result + where + E::T: pallet_assets::Config, + ::RuntimeCall: From>, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_approve_transfer" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, delegate, amount): ( + ::AssetId, + ::AccountId, + ::Balance, + ) = env.read_as()?; + + let ext = env.ext(); + let call: ::RuntimeCall = + pallet_assets::Call::::approve_transfer { + id: asset_id.into(), + delegate: ::Lookup::unlookup(delegate), + amount, + } + .into(); + ext.call_runtime(call).map_err(|e| e.error)?; + + Ok(RetVal::Converging(0)) + } + + fn asset_cancel_approval( + env: Environment, + ) -> Result + where + E::T: pallet_assets::Config, + ::RuntimeCall: From>, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_cancel_approval" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, delegate): ( + ::AssetId, + ::AccountId, + ) = env.read_as()?; + + let ext = env.ext(); + let call: ::RuntimeCall = + pallet_assets::Call::::cancel_approval { + id: asset_id.into(), + delegate: ::Lookup::unlookup(delegate), + } + .into(); + ext.call_runtime(call).map_err(|e| e.error)?; + + Ok(RetVal::Converging(0)) + } + + fn asset_transfer(env: Environment) -> Result + where + E::T: pallet_assets::Config, + ::RuntimeCall: From>, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_transfer" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, target, amount): ( + ::AssetId, + ::AccountId, + ::Balance, + ) = env.read_as()?; + + let ext = env.ext(); + let call: ::RuntimeCall = + pallet_assets::Call::::transfer { + id: asset_id.into(), + target: ::Lookup::unlookup(target), + amount, + } + .into(); + ext.call_runtime(call).map_err(|e| e.error)?; + + Ok(RetVal::Converging(0)) + } + + fn asset_transfer_approved( + env: Environment, + ) -> Result + where + E::T: pallet_assets::Config, + ::RuntimeCall: From>, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_transfer_approved" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, owner, destination, amount): ( + ::AssetId, + ::AccountId, + ::AccountId, + ::Balance, + ) = env.read_as()?; + + let ext = env.ext(); + let call: ::RuntimeCall = + pallet_assets::Call::::transfer_approved { + id: asset_id.into(), + owner: ::Lookup::unlookup(owner), + destination: ::Lookup::unlookup(destination), + amount, + } + .into(); + ext.call_runtime(call).map_err(|e| e.error)?; + + Ok(RetVal::Converging(0)) + } + + fn asset_transfer_keep_alive( + env: Environment, + ) -> Result + where + E::T: pallet_assets::Config, + ::RuntimeCall: From>, + { + trace!( + target: "runtime", + "[ChainExtension]|call|asset_transfer_keep_alive" + ); + + let mut env = env.buf_in_buf_out(); + let (asset_id, target, amount): ( + ::AssetId, + ::AccountId, + ::Balance, + ) = env.read_as()?; + + let ext = env.ext(); + let call: ::RuntimeCall = + pallet_assets::Call::::transfer_keep_alive { + id: asset_id.into(), + target: ::Lookup::unlookup(target), + amount, + } + .into(); + ext.call_runtime(call).map_err(|e| e.error)?; + + Ok(RetVal::Converging(0)) + } } impl ChainExtension for LiberlandExtension where T: pallet_llm::Config + pallet_contracts::Config, ::RuntimeCall: From>, + ::RuntimeCall: From>, { - fn call(&mut self, env: Environment) -> Result + fn call(&mut self, env: Environment) -> Result where - E::T: pallet_llm::Config + pallet_contracts::Config, - ::RuntimeCall: From>, + E: Ext, { let func_id = env.func_id(); match func_id { - 1 => self.llm_force_transfer::(env), + 1 => Self::llm_force_transfer::(env), + 2 => Self::asset_balance_of::(env), + 3 => Self::asset_total_supply_of::(env), + 4 => Self::asset_approve_transfer::(env), + 5 => Self::asset_cancel_approval::(env), + 6 => Self::asset_transfer::(env), + 7 => Self::asset_transfer_approved::(env), + 8 => Self::asset_transfer_keep_alive::(env), _ => { error!("Called an unregistered `func_id`: {:}", func_id); return Err(DispatchError::Other("Unimplemented func_id")); diff --git a/substrate/bin/node/runtime/src/impls.rs b/substrate/bin/node/runtime/src/impls.rs index 61cb571b1..2e68be280 100644 --- a/substrate/bin/node/runtime/src/impls.rs +++ b/substrate/bin/node/runtime/src/impls.rs @@ -496,7 +496,14 @@ pub struct ContractsCallFilter; impl Contains for ContractsCallFilter { fn contains(c: &RuntimeCall) -> bool { - matches!(c, RuntimeCall::LLM(pallet_llm::Call::force_transfer { .. })) + matches!(c, + RuntimeCall::LLM(pallet_llm::Call::force_transfer { .. }) | + RuntimeCall::Assets(pallet_assets::Call::approve_transfer { .. }) | + RuntimeCall::Assets(pallet_assets::Call::cancel_approval { .. }) | + RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) | + RuntimeCall::Assets(pallet_assets::Call::transfer_approved { .. }) | + RuntimeCall::Assets(pallet_assets::Call::transfer_keep_alive { .. }) + ) } }