Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Asset Conversion pallet (#12984)
Browse files Browse the repository at this point in the history
* Add pallet dex

* Fmt

* Add RPC endpoint

* Fix RPC

* Fix the build

* Some more fixes

* Add a method to topup pallet's account

* Add support for multi-currency into Uniques

* Fix the build

* Add [transactional] + setup() + fix balances

* Improve tests

* Fix price quotation

* Code clean up

* Validate swaps

* Fmt

* Update README

* add test

* mint LP assets in a different instance

* remove transactional as now the default

AssetsLocal renamed to Assets

* merge master

* Revert "Merge master"

* fix tests post merge.

* attempt to set create origin

* Internally allocate lp asset id.

* Simplify

* Bump to be in line

* additional bumps to make compile

* fix compile

* less bounds

* use fungible crates

* multiasset enum

* only allow native currency pairs

* added slippage tests

* transfer into separate method

(Also fee not set in 2 places now.)

Added test where lp and user are different users.

* Add benchmarks + weights

* Typos

* Clean up

* More tests,

split error into two because it wasn't clear which parameter.

renamed liquidity to lp_tokens_minted or lp_tokens_burned in events.

* tighten up naming

* Default, zero, square root traits not needed

Also let's not force people to be compact

* add keep-alive param

* add insufficient liquidity test

* Fix quote() to support u64

* Avoid recording balances twice

* cargo fmt

* Didn't mean to change error type

* temp

* Less

* Rework get_amount_in/get_amount_out

* Convert other places

* Rework the last piece

* Typo

* Fix benchmarks

* use hash trait

* Extract a native asset check into the runtime setting

* Don't set the metadata

* Remove spec file

* Enable multi-assets swaps by default

* Refactor conversion into u128

* Add path param to swap_token_for_exact_tokens

* Fix typo + a bit of refactoring

* Implement path param for swap_exact_tokens_for_tokens()

* Deref

* Minor fixes

* Add test with sensible scale values

* Use .windows()

* Fix benchmarks

* update docs

* Fix everything :)

* Chore

* Revert

* Chore

* prev way of creating sub accounts lead to collisions

* Update frame/dex/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Update frame/dex/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Chore

* Fmt fix on Uniques

* add call_index

bring code up to date with latest master

* revert readme changes

* add cr

* revert uniques changes

* reducing noise

* no need for deadline (#12990)

(there's generic transaction deadline functionality already)

* fix kitchen sink (#12991)

* fix kitchen sink

* Only the dex can mint lp_tokens

* add BenchmarkHelper for second instance (#12998)

* update mock to latest master

* less indirections (#13012)

* remove dex PR's custom RPC (#13050)

* As we have state_call we don't need a custom RPC

* fix docs

* no longer a need to upgrade rpc version (#13053)

* add CallbackHadle

* quote bugfix (#13191)

quote was giving same price in both directions as we were inverting needlessly.

* merging in dex specific changes due to pay by dex

* update lock file

* merging in kitchen sink changes

* Add get_reserves() api method

* Partial updating of the benchmarks

* Fix tests

* clippy

* Temp fix weights

* Fix benchmarks

* Add pool setup fee

* Money upfront

* Address some comments

* Use u128 in mock

* Fix benchmarks

* Change error message

* Update comments

* Change error names

* Implement PartialOrd for NativeOrAssetId

* add note

* Update errors

* More tests for assets sorting

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_dex

* Change the way we generate pool accounts

* Improve the liquidity removal method

* Extract MintMinLiquidity to config, rework all tests

* Add comments

* Validate provided amount

* Rename to asset-conversion

* Validate ED

* Improve handling the ED related errors

* typos

* Try to fix benchmarks

* Another try

* Another day, another try

* Fix benchmarks

* Expose fee related params

* Validate token's minimal amount the same way as ED

* fix typo

* Use longer path for swaps in benchmarks

* need to ref sp_std's vec.

* Remove From<u32> requirement when benchmarking

* impl BenchmarkHelper for ()

* only for runtime benchmarks

* MultiLocation: !MaybeDisplay

Looks like we might not need this bound from initial testing.

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Add documentation links

* add collision test

* Revert "[Enhancement] Throw an error when there are too many pallets (#13763)"

This reverts commit 9b8e6e7.

* [Enhancement] Throw an error when there are too many pallets (#13763)

* [Enhancement] Throw an error when there are too many pallets

* fix ui test

* fix PR comments

* Update frame/support/procedural/src/construct_runtime/mod.rs

Co-authored-by: Bastian Köcher <git@kchr.de>

* Update frame/support/procedural/src/construct_runtime/mod.rs

Co-authored-by: Bastian Köcher <git@kchr.de>

* ".git/.scripts/commands/fmt/fmt.sh"

---------

Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: command-bot <>

* add benchmark helper

+ doc fix

* cargo fmt

* Fix adding liquidity to non-empty pool

* Fix compilation error

* Fix params ordering issue

* additional docs

* The swap path elements should be unique

* Fix account collision

* Validate all the pool in a swap path are unique

* Change the way we add liquidity to empty pools

* Improve docs

* remove unnessisary Display impl

* cargo fmt

* remove unused imports

* Make api consistent

* Chore

* Touch the pool account so it could hold the pair tokens

* Check the balance before touching the pool's account

* Introduce liquidity provision fee

* Touch the pool acc one more time

* Apply suggestions from code review

Co-authored-by: Sam Johnson <sam@durosoft.com>

* Update frame/asset-conversion/README.md

Co-authored-by: Sam Johnson <sam@durosoft.com>

* Use ContainsPair instead of the balance checker

* Remove old Currency trait

* Add liquidity withdrawal fee

* Update docs

* Use 0 withdrawal fee in mock

* Rename vars

* asset id not clone

* fix: shadow var was being used

* correct tests

* fix benches

* merge master

* neater

---------

Co-authored-by: Jegor Sidorenko <jegor@parity.io>
Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>
Co-authored-by: parity-processbot <>
Co-authored-by: Roman Useinov <roman.useinov@gmail.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Sam Johnson <sam@durosoft.com>
  • Loading branch information
6 people authored and Ank4n committed Jul 8, 2023
1 parent 0759846 commit 017da7c
Show file tree
Hide file tree
Showing 20 changed files with 3,727 additions and 10 deletions.
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ members = [
"client/transaction-pool/api",
"client/utils",
"frame/alliance",
"frame/asset-conversion",
"frame/assets",
"frame/atomic-swap",
"frame/aura",
Expand Down
1 change: 1 addition & 0 deletions bin/node/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ sc-storage-monitor = { version = "0.1.0", path = "../../../client/storage-monito
frame-system = { version = "4.0.0-dev", path = "../../../frame/system" }
frame-system-rpc-runtime-api = { version = "4.0.0-dev", path = "../../../frame/system/rpc/runtime-api" }
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" }
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets/" }
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment/" }
pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" }
Expand Down
1 change: 1 addition & 0 deletions bin/node/cli/src/chain_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ pub fn testnet_genesis(
assets: vec![(9, get_account_id_from_seed::<sr25519::Public>("Alice"), true, 1)],
..Default::default()
},
pool_assets: Default::default(),
transaction_storage: Default::default(),
transaction_payment: Default::default(),
alliance: Default::default(),
Expand Down
7 changes: 7 additions & 0 deletions bin/node/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ scale-info = { version = "2.5.0", default-features = false, features = ["derive"
static_assertions = "1.1.0"
log = { version = "0.4.17", default-features = false }

# pallet-asset-conversion: turn on "num-traits" feature
primitive-types = { version = "0.12.0", default-features = false, features = ["codec", "scale-info", "num-traits"] }

# primitives
sp-authority-discovery = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/authority-discovery" }
sp-consensus-babe = { version = "0.10.0-dev", default-features = false, path = "../../../primitives/consensus/babe" }
Expand Down Expand Up @@ -54,6 +57,7 @@ frame-election-provider-support = { version = "4.0.0-dev", default-features = fa
frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/system/rpc/runtime-api/" }
frame-try-runtime = { version = "0.10.0-dev", default-features = false, path = "../../../frame/try-runtime", optional = true }
pallet-alliance = { version = "4.0.0-dev", default-features = false, path = "../../../frame/alliance" }
pallet-asset-conversion = { version = "4.0.0-dev", default-features = false, path = "../../../frame/asset-conversion" }
pallet-asset-rate = { version = "4.0.0-dev", default-features = false, path = "../../../frame/asset-rate" }
pallet-assets = { version = "4.0.0-dev", default-features = false, path = "../../../frame/assets" }
pallet-authority-discovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/authority-discovery" }
Expand Down Expand Up @@ -137,6 +141,7 @@ std = [
"frame-system-benchmarking?/std",
"frame-election-provider-support/std",
"sp-authority-discovery/std",
"pallet-asset-conversion/std",
"pallet-assets/std",
"pallet-authority-discovery/std",
"pallet-authorship/std",
Expand Down Expand Up @@ -236,6 +241,7 @@ runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"pallet-alliance/runtime-benchmarks",
"pallet-asset-conversion/runtime-benchmarks",
"pallet-assets/runtime-benchmarks",
"pallet-babe/runtime-benchmarks",
"pallet-bags-list/runtime-benchmarks",
Expand Down Expand Up @@ -297,6 +303,7 @@ try-runtime = [
"frame-system/try-runtime",
"frame-support/try-runtime",
"pallet-alliance/try-runtime",
"pallet-asset-conversion/try-runtime",
"pallet-assets/try-runtime",
"pallet-authority-discovery/try-runtime",
"pallet-authorship/try-runtime",
Expand Down
100 changes: 93 additions & 7 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ use frame_election_provider_support::{
use frame_support::{
construct_runtime,
dispatch::DispatchClass,
instances::{Instance1, Instance2},
ord_parameter_types,
pallet_prelude::Get,
parameter_types,
traits::{
Expand All @@ -48,10 +50,11 @@ use frame_support::{
};
use frame_system::{
limits::{BlockLength, BlockWeights},
EnsureRoot, EnsureRootWithSuccess, EnsureSigned, EnsureWithSuccess,
EnsureRoot, EnsureRootWithSuccess, EnsureSigned, EnsureSignedBy, EnsureWithSuccess,
};
pub use node_primitives::{AccountId, Signature};
use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Index, Moment};
use pallet_asset_conversion::{NativeOrAssetId, NativeOrAssetIdConverter};
use pallet_election_provider_multi_phase::SolutionAccuracyOf;
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
use pallet_nfts::PalletFeatures;
Expand All @@ -69,8 +72,8 @@ use sp_runtime::{
curve::PiecewiseLinear,
generic, impl_opaque_keys,
traits::{
self, BlakeTwo256, Block as BlockT, Bounded, ConvertInto, NumberFor, OpaqueKeys,
SaturatedConversion, StaticLookup,
self, AccountIdConversion, BlakeTwo256, Block as BlockT, Bounded, ConvertInto, NumberFor,
OpaqueKeys, SaturatedConversion, StaticLookup,
},
transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity},
ApplyExtrinsicResult, FixedPointNumber, FixedU128, Perbill, Percent, Permill, Perquintill,
Expand Down Expand Up @@ -482,7 +485,7 @@ impl pallet_asset_tx_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Fungibles = Assets;
type OnChargeAssetTransaction = pallet_asset_tx_payment::FungiblesAdapter<
pallet_assets::BalanceToAssetBalance<Balances, Runtime, ConvertInto>,
pallet_assets::BalanceToAssetBalance<Balances, Runtime, ConvertInto, Instance1>,
CreditToBlockAuthor,
>;
}
Expand Down Expand Up @@ -1473,7 +1476,7 @@ parameter_types! {
pub const MetadataDepositPerByte: Balance = 1 * DOLLARS;
}

impl pallet_assets::Config for Runtime {
impl pallet_assets::Config<Instance1> for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = u128;
type AssetId = u32;
Expand All @@ -1496,6 +1499,66 @@ impl pallet_assets::Config for Runtime {
type BenchmarkHelper = ();
}

ord_parameter_types! {
pub const AssetConversionOrigin: AccountId = AccountIdConversion::<AccountId>::into_account_truncating(&AssetConversionPalletId::get());
}

impl pallet_assets::Config<Instance2> for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = u128;
type AssetId = u32;
type AssetIdParameter = codec::Compact<u32>;
type Currency = Balances;
type CreateOrigin = AsEnsureOriginWithArg<EnsureSignedBy<AssetConversionOrigin, AccountId>>;
type ForceOrigin = EnsureRoot<AccountId>;
type AssetDeposit = AssetDeposit;
type AssetAccountDeposit = ConstU128<DOLLARS>;
type MetadataDepositBase = MetadataDepositBase;
type MetadataDepositPerByte = MetadataDepositPerByte;
type ApprovalDeposit = ApprovalDeposit;
type StringLimit = StringLimit;
type Freezer = ();
type Extra = ();
type WeightInfo = pallet_assets::weights::SubstrateWeight<Runtime>;
type RemoveItemsLimit = ConstU32<1000>;
type CallbackHandle = ();
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

parameter_types! {
pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon");
pub AllowMultiAssetPools: bool = true;
pub const PoolSetupFee: Balance = 1 * DOLLARS; // should be more or equal to the existential deposit
pub const MintMinLiquidity: Balance = 100; // 100 is good enough when the main currency has 10-12 decimals.
pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); // should be non-zero if AllowMultiAssetPools is true, otherwise can be zero.
}

impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type AssetBalance = <Self as pallet_balances::Config>::Balance;
type HigherPrecisionBalance = sp_core::U256;
type Assets = Assets;
type Balance = u128;
type PoolAssets = PoolAssets;
type AssetId = <Self as pallet_assets::Config<Instance1>>::AssetId;
type MultiAssetId = NativeOrAssetId<u32>;
type PoolAssetId = <Self as pallet_assets::Config<Instance2>>::AssetId;
type PalletId = AssetConversionPalletId;
type LPFee = ConstU32<3>; // means 0.3%
type PoolSetupFee = PoolSetupFee;
type PoolSetupFeeReceiver = AssetConversionOrigin;
type LiquidityWithdrawalFee = LiquidityWithdrawalFee;
type WeightInfo = pallet_asset_conversion::weights::SubstrateWeight<Runtime>;
type AllowMultiAssetPools = AllowMultiAssetPools;
type MaxSwapPathLength = ConstU32<4>;
type MintMinLiquidity = MintMinLiquidity;
type MultiAssetIdConverter = NativeOrAssetIdConverter<u32>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

parameter_types! {
pub const QueueCount: u32 = 300;
pub const MaxQueueLen: u32 = 1000;
Expand Down Expand Up @@ -1617,7 +1680,7 @@ impl pallet_nft_fractionalization::Config for Runtime {
type NftCollectionId = <Self as pallet_nfts::Config>::CollectionId;
type NftId = <Self as pallet_nfts::Config>::ItemId;
type AssetBalance = <Self as pallet_balances::Config>::Balance;
type AssetId = <Self as pallet_assets::Config>::AssetId;
type AssetId = <Self as pallet_assets::Config<Instance1>>::AssetId;
type Assets = Assets;
type Nfts = Nfts;
type PalletId = NftFractionalizationPalletId;
Expand Down Expand Up @@ -1838,7 +1901,8 @@ construct_runtime!(
Multisig: pallet_multisig,
Bounties: pallet_bounties,
Tips: pallet_tips,
Assets: pallet_assets,
Assets: pallet_assets::<Instance1>,
PoolAssets: pallet_assets::<Instance2>,
Mmr: pallet_mmr,
Lottery: pallet_lottery,
Nis: pallet_nis,
Expand All @@ -1861,6 +1925,7 @@ construct_runtime!(
NominationPools: pallet_nomination_pools,
RankedPolls: pallet_referenda::<Instance2>,
RankedCollective: pallet_ranked_collective,
AssetConversion: pallet_asset_conversion,
FastUnstake: pallet_fast_unstake,
MessageQueue: pallet_message_queue,
Pov: frame_benchmarking_pallet_pov,
Expand Down Expand Up @@ -1951,6 +2016,7 @@ mod benches {
[pallet_contracts, Contracts]
[pallet_core_fellowship, CoreFellowship]
[pallet_democracy, Democracy]
[pallet_asset_conversion, AssetConversion]
[pallet_election_provider_multi_phase, ElectionProviderMultiPhase]
[pallet_election_provider_support_benchmarking, EPSBench::<Runtime>]
[pallet_elections_phragmen, Elections]
Expand Down Expand Up @@ -2288,6 +2354,26 @@ impl_runtime_apis! {
}
}

impl pallet_asset_conversion::AssetConversionApi<
Block,
Balance,
u128,
NativeOrAssetId<u32>
> for Runtime
{
fn quote_price_exact_tokens_for_tokens(asset1: NativeOrAssetId<u32>, asset2: NativeOrAssetId<u32>, amount: u128, include_fee: bool) -> Option<Balance> {
AssetConversion::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee)
}

fn quote_price_tokens_for_exact_tokens(asset1: NativeOrAssetId<u32>, asset2: NativeOrAssetId<u32>, amount: u128, include_fee: bool) -> Option<Balance> {
AssetConversion::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee)
}

fn get_reserves(asset1: NativeOrAssetId<u32>, asset2: NativeOrAssetId<u32>) -> Option<(Balance, Balance)> {
AssetConversion::get_reserves(&asset1, &asset2).ok()
}
}

impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi<Block, Balance, RuntimeCall>
for Runtime
{
Expand Down
1 change: 1 addition & 0 deletions bin/node/testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ frame-system = { version = "4.0.0-dev", path = "../../../frame/system" }
node-executor = { version = "3.0.0-dev", path = "../executor" }
node-primitives = { version = "2.0.0", path = "../primitives" }
kitchensink-runtime = { version = "3.0.0-dev", path = "../runtime" }
pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" }
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets" }
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
Expand Down
1 change: 1 addition & 0 deletions bin/node/testing/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec<AccountId>) -> Gen
society: SocietyConfig { members: vec![alice(), bob()], pot: 0, max_members: 999 },
vesting: Default::default(),
assets: AssetsConfig { assets: vec![(9, alice(), true, 1)], ..Default::default() },
pool_assets: Default::default(),
transaction_storage: Default::default(),
transaction_payment: Default::default(),
alliance: Default::default(),
Expand Down
52 changes: 52 additions & 0 deletions frame/asset-conversion/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
name = "pallet-asset-conversion"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "FRAME asset conversion pallet"
readme = "README.md"

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true }
scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" }
sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" }
sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
sp-arithmetic = { version = "6.0.0", default-features = false, path = "../../primitives/arithmetic" }

[dev-dependencies]
pallet-balances = { version = "4.0.0-dev", path = "../balances" }
pallet-assets = { version = "4.0.0-dev", path = "../assets" }
primitive-types = { version = "0.12.0", default-features = false, features = ["codec", "scale-info", "num-traits"] }
sp-std = { version = "5.0.0", path = "../../primitives/std" }
sp-core = { version = "7.0.0", path = "../../primitives/core" }
sp-io = { version = "7.0.0", path = "../../primitives/io" }

[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
"sp-std/std",
"sp-runtime/std",
"sp-arithmetic/std"
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = ["frame-support/try-runtime"]
Loading

0 comments on commit 017da7c

Please sign in to comment.