diff --git a/pallets/corporate-actions/src/distribution/mod.rs b/pallets/corporate-actions/src/distribution/mod.rs index 018d17175c..06b746d33b 100644 --- a/pallets/corporate-actions/src/distribution/mod.rs +++ b/pallets/corporate-actions/src/distribution/mod.rs @@ -87,6 +87,7 @@ use polymesh_primitives::{ SecondaryKey, Ticker, }; use scale_info::TypeInfo; +use sp_runtime::traits::Zero; #[cfg(feature = "std")] use sp_runtime::{Deserialize, Serialize}; use sp_std::prelude::*; @@ -202,6 +203,8 @@ decl_module! { /// - `InsufficientPortfolioBalance` if `portfolio` has less than `amount` of `currency`. /// - `InsufficientBalance` if the protocol fee couldn't be charged. /// - `CANotBenefit` if the CA is not of kind PredictableBenefit/UnpredictableBenefit + /// - `DistributionAmountIsZero` if the `amount` is zero. + /// - `DistributionPerShareIsZero` if the `per_share` is zero. /// /// # Permissions /// * Asset @@ -217,15 +220,8 @@ decl_module! { payment_at: Moment, expires_at: Option, ) -> DispatchResult { - let PermissionedCallOriginData { - primary_did: agent, - secondary_key, - .. - } = >::ensure_agent_asset_perms(origin, ca_id.ticker)?; - - Self::unsafe_distribute( - agent, - secondary_key, + Self::base_distribute( + origin, ca_id, portfolio, currency, @@ -261,8 +257,7 @@ decl_module! { /// - Other errors can occur if the compliance manager rejects the transfer. #[weight = ::DistWeightInfo::claim(T::MaxTargetIds::get(), T::MaxDidWhts::get())] pub fn claim(origin, ca_id: CAId) { - let did = >::ensure_perms(origin)?; - Self::transfer_benefit(did.for_event(), did, ca_id)?; + Self::base_claim(origin, ca_id)?; } /// Push benefit of an ongoing distribution to the given `holder`. @@ -291,8 +286,7 @@ decl_module! { /// - Other errors can occur if the compliance manager rejects the transfer. #[weight = ::DistWeightInfo::push_benefit(T::MaxTargetIds::get(), T::MaxDidWhts::get())] pub fn push_benefit(origin, ca_id: CAId, holder: IdentityId) { - let agent = >::ensure_perms(origin, ca_id.ticker)?.for_event(); - Self::transfer_benefit(agent, holder, ca_id)?; + Self::base_push_benefit(origin, ca_id, holder)?; } /// Assuming a distribution has expired, @@ -308,27 +302,7 @@ decl_module! { /// - `NotExpired` if `now < expiry`. #[weight = ::DistWeightInfo::reclaim()] pub fn reclaim(origin, ca_id: CAId) { - // Ensure distribution is created, they haven't reclaimed, and that expiry has passed. - // CA must be authorized and be the custodian. - let PermissionedCallOriginData { - primary_did: agent, - secondary_key, - .. - } = >::ensure_agent_asset_perms(origin.clone(), ca_id.ticker)?; - let dist = Self::ensure_distribution_exists(ca_id)?; - ensure!(!dist.reclaimed, Error::::AlreadyReclaimed); - ensure!(expired(dist.expires_at, >::now_unix()), Error::::NotExpired); - >::ensure_portfolio_custody_and_permission(dist.from, agent, secondary_key.as_ref())?; - - // Unlock `remaining` of `currency` from DID's portfolio. - // This won't fail, as we've already locked the requisite amount prior. - Self::unlock(&dist, dist.remaining)?; - - // Zero `remaining` + note that we've reclaimed. - Distributions::insert(ca_id, Distribution { reclaimed: true, remaining:0u32.into(), ..dist }); - - // Emit event. - Self::deposit_event(Event::Reclaimed(agent.for_event(), ca_id, dist.remaining)); + Self::base_reclaim(origin, ca_id)?; } /// Removes a distribution that hasn't started yet, @@ -344,9 +318,7 @@ decl_module! { /// - `DistributionStarted` if `payment_at <= now`. #[weight = ::DistWeightInfo::remove_distribution()] pub fn remove_distribution(origin, ca_id: CAId) { - let agent = >::ensure_perms(origin, ca_id.ticker)?.for_event(); - let dist = Self::ensure_distribution_exists(ca_id)?; - Self::remove_distribution_base(agent, ca_id, &dist)?; + Self::base_remove_distribution(origin, ca_id)?; } } } @@ -405,12 +377,107 @@ decl_error! { DistributionStarted, /// A distribution has insufficient remaining amount of currency to distribute. InsufficientRemainingAmount, + /// Distribution `amount` cannot be zero. + DistributionAmountIsZero, + /// Distribution `per_share` cannot be zero. + DistributionPerShareIsZero, } } impl Module { + fn base_distribute( + origin: T::Origin, + ca_id: CAId, + portfolio: Option, + currency: Ticker, + per_share: Balance, + amount: Balance, + payment_at: Moment, + expires_at: Option, + ) -> DispatchResult { + let PermissionedCallOriginData { + primary_did: agent, + secondary_key, + .. + } = >::ensure_agent_asset_perms(origin, ca_id.ticker)?; + + Self::unverified_distribute( + agent, + secondary_key, + ca_id, + portfolio, + currency, + per_share, + amount, + payment_at, + expires_at, + ) + } + + fn base_claim(origin: T::Origin, ca_id: CAId) -> DispatchResult { + let did = >::ensure_perms(origin)?; + Self::transfer_benefit(did.for_event(), did, ca_id)?; + Ok(()) + } + + fn base_push_benefit(origin: T::Origin, ca_id: CAId, holder: IdentityId) -> DispatchResult { + let agent = >::ensure_perms(origin, ca_id.ticker)?.for_event(); + Self::transfer_benefit(agent, holder, ca_id)?; + Ok(()) + } + + fn base_reclaim(origin: T::Origin, ca_id: CAId) -> DispatchResult { + // Ensure distribution is created, they haven't reclaimed, and that expiry has passed. + // CA must be authorized and be the custodian. + let PermissionedCallOriginData { + primary_did: agent, + secondary_key, + .. + } = >::ensure_agent_asset_perms(origin.clone(), ca_id.ticker)?; + let dist = Self::ensure_distribution_exists(ca_id)?; + ensure!(!dist.reclaimed, Error::::AlreadyReclaimed); + ensure!( + expired(dist.expires_at, >::now_unix()), + Error::::NotExpired + ); + >::ensure_portfolio_custody_and_permission( + dist.from, + agent, + secondary_key.as_ref(), + )?; + + // Unlock `remaining` of `currency` from DID's portfolio. + // This won't fail, as we've already locked the requisite amount prior. + Self::unlock(&dist, dist.remaining)?; + + // Zero `remaining` + note that we've reclaimed. + Distributions::insert( + ca_id, + Distribution { + reclaimed: true, + remaining: 0u32.into(), + ..dist + }, + ); + + // Emit event. + Self::deposit_event(Event::Reclaimed(agent.for_event(), ca_id, dist.remaining)); + + Ok(()) + } + + fn base_remove_distribution(origin: T::Origin, ca_id: CAId) -> DispatchResult { + let agent = >::ensure_perms(origin, ca_id.ticker)?.for_event(); + let dist = Self::ensure_distribution_exists(ca_id)?; + Self::unverified_remove_distribution(agent, ca_id, &dist)?; + + Ok(()) + } + /// Kill the distribution identified by `ca_id`. - crate fn remove_distribution_base( + /// + /// Unlike `base_remove_distribution`, this won't check permissions and that the dist exists. + crate fn unverified_remove_distribution( agent: EventDid, ca_id: CAId, dist: &Distribution, @@ -531,7 +598,10 @@ impl Module { Ok(dist) } - pub fn unsafe_distribute( + /// Create a capital distribution. + /// + /// Unlike `base_distribute`, this won't check permissions. + crate fn unverified_distribute( agent: IdentityId, secondary_key: Option>, ca_id: CAId, @@ -542,6 +612,10 @@ impl Module { payment_at: Moment, expires_at: Option, ) -> DispatchResult { + // Ensure valid `amount` and `per_share`. + ensure!(!amount.is_zero(), Error::::DistributionAmountIsZero); + ensure!(!per_share.is_zero(), Error::::DistributionPerShareIsZero); + // Ensure that any expiry date doesn't come before the payment date. ensure!( !expired(expires_at, payment_at), diff --git a/pallets/corporate-actions/src/lib.rs b/pallets/corporate-actions/src/lib.rs index 6b05ebc966..b0ae198cc5 100644 --- a/pallets/corporate-actions/src/lib.rs +++ b/pallets/corporate-actions/src/lib.rs @@ -652,7 +652,7 @@ decl_module! { } CAKind::PredictableBenefit | CAKind::UnpredictableBenefit => { if let Some(dist) = >::distributions(ca_id) { - >::remove_distribution_base(agent, ca_id, &dist)?; + >::unverified_remove_distribution(agent, ca_id, &dist)?; } } } @@ -761,7 +761,7 @@ decl_module! { withholding_tax )?; - >::unsafe_distribute( + >::unverified_distribute( agent, secondary_key, ca_id, diff --git a/pallets/runtime/ci/src/runtime.rs b/pallets/runtime/ci/src/runtime.rs index c7cb224065..eb0d4d8d77 100644 --- a/pallets/runtime/ci/src/runtime.rs +++ b/pallets/runtime/ci/src/runtime.rs @@ -62,7 +62,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_version: 5_000_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 2, + transaction_version: 3, }; parameter_types! { diff --git a/pallets/runtime/develop/src/runtime.rs b/pallets/runtime/develop/src/runtime.rs index ed9115e3af..a2cb903d9c 100644 --- a/pallets/runtime/develop/src/runtime.rs +++ b/pallets/runtime/develop/src/runtime.rs @@ -60,7 +60,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_version: 5_000_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 2, + transaction_version: 3, }; parameter_types! { diff --git a/pallets/runtime/mainnet/src/runtime.rs b/pallets/runtime/mainnet/src/runtime.rs index 4de9da5e3c..df03fb7fee 100644 --- a/pallets/runtime/mainnet/src/runtime.rs +++ b/pallets/runtime/mainnet/src/runtime.rs @@ -57,7 +57,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_version: 5_000_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 2, + transaction_version: 3, }; parameter_types! { diff --git a/pallets/runtime/testnet/src/runtime.rs b/pallets/runtime/testnet/src/runtime.rs index 909e10645d..3db110e41c 100644 --- a/pallets/runtime/testnet/src/runtime.rs +++ b/pallets/runtime/testnet/src/runtime.rs @@ -62,7 +62,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_version: 5_000_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 2, + transaction_version: 3, }; parameter_types! { diff --git a/pallets/runtime/tests/src/corporate_actions_test.rs b/pallets/runtime/tests/src/corporate_actions_test.rs index 22a4bf940c..626b4b54c0 100644 --- a/pallets/runtime/tests/src/corporate_actions_test.rs +++ b/pallets/runtime/tests/src/corporate_actions_test.rs @@ -253,6 +253,7 @@ fn only_caa_authorized() { transfer(&ticker, owner, other); let currency = create_asset(b"BETA", owner); + transfer(¤cy, owner, caa); macro_rules! checks { ($user:expr, $assert:ident $(, $tail:expr)?) => { @@ -301,7 +302,7 @@ fn only_caa_authorized() { // ..., `distribute`, let id = next_ca_id(ticker); $assert!(mk_ca(CAKind::UnpredictableBenefit) $(, $tail)?); - $assert!(Dist::distribute($user.origin(), id, None, currency, 0, 0, 3000, None) $(, $tail)?); + $assert!(Dist::distribute($user.origin(), id, None, currency, 1, 1, 3000, None) $(, $tail)?); // ..., and `remove_distribution`. $assert!(Dist::remove_distribution($user.origin(), id) $(, $tail)?); }; @@ -837,7 +838,7 @@ fn remove_ca_works() { None, currency, 2, - 0, + 2, 3000, None, )); @@ -851,8 +852,8 @@ fn remove_ca_works() { from: PortfolioId::default_portfolio(owner.did), currency, per_share: 2, - amount: 0, - remaining: 0, + amount: 2, + remaining: 2, reclaimed: false, payment_at: 3000, expires_at: None, @@ -1013,8 +1014,8 @@ fn change_record_date_works() { id, None, create_asset(b"BETA", owner), - 0, - 0, + 1, + 2, 5000, None, )); @@ -1756,7 +1757,7 @@ fn dist_distribute_works() { // Test no CA at id. let id = next_ca_id(ticker); assert_noop!( - Dist::distribute(owner.origin(), id, None, currency, 0, 0, 1, None), + Dist::distribute(owner.origin(), id, None, currency, 1, 1, 1, None), Error::NoSuchCA ); @@ -1764,39 +1765,54 @@ fn dist_distribute_works() { Timestamp::set_timestamp(2); let id2 = dist_ca(owner, ticker, Some(2)).unwrap(); + + // Test same-asset. let id3 = dist_ca(owner, ticker, Some(2)).unwrap(); assert_ok!(Dist::distribute( owner.origin(), id3, None, ticker, - 0, - 0, + 1, + 1, 5, None )); + // Test expiry. for &(pay, expiry) in &[(5, 5), (6, 5)] { assert_noop!( - Dist::distribute(owner.origin(), id2, None, currency, 0, 0, pay, Some(expiry)), + Dist::distribute(owner.origin(), id2, None, currency, 1, 1, pay, Some(expiry)), DistError::ExpiryBeforePayment ); } Timestamp::set_timestamp(5); + + // Test `amount == 0`. + assert_noop!( + Dist::distribute(owner.origin(), id2, None, currency, 1, 0, 5, Some(6)), + DistError::DistributionAmountIsZero + ); + // Test `per_share == 0`. + assert_noop!( + Dist::distribute(owner.origin(), id2, None, currency, 0, 1, 5, Some(6)), + DistError::DistributionPerShareIsZero + ); + assert_ok!(Dist::distribute( owner.origin(), id2, None, currency, - 0, - 0, + 1, + 1, 5, Some(6) )); // Distribution already exists. assert_noop!( - Dist::distribute(owner.origin(), id2, None, currency, 0, 0, 5, None), + Dist::distribute(owner.origin(), id2, None, currency, 1, 1, 5, None), DistError::AlreadyExists ); @@ -1806,8 +1822,8 @@ fn dist_distribute_works() { id1, None, currency, - 0, - 0, + 1, + 1, 4, None )); @@ -1816,14 +1832,14 @@ fn dist_distribute_works() { let id = dist_ca(owner, ticker, Some(5)).unwrap(); let num = PortfolioNumber(42); assert_noop!( - Dist::distribute(owner.origin(), id, Some(num), currency, 0, 0, 5, None), + Dist::distribute(owner.origin(), id, Some(num), currency, 1, 1, 5, None), PError::PortfolioDoesNotExist ); // No custody over portfolio. let custody = |who: User| Custodian::insert(PortfolioId::default_portfolio(owner.did), who.did); - let dist = |id| Dist::distribute(owner.origin(), id, None, currency, 0, 0, 6, None); + let dist = |id| Dist::distribute(owner.origin(), id, None, currency, 1, 1, 6, None); custody(other); assert_noop!(dist(id), PError::UnauthorizedCustodian); custody(owner); @@ -1845,7 +1861,7 @@ fn dist_distribute_works() { // Record date after start. let dist = - |id, start| Dist::distribute(owner.origin(), id, None, currency, 0, 0, start, None); + |id, start| Dist::distribute(owner.origin(), id, None, currency, 1, 1, start, None); let id = dist_ca(owner, ticker, Some(5000)).unwrap(); assert_noop!(dist(id, 4999), Error::RecordDateAfterStart); assert_ok!(dist(id, 5000)); @@ -1892,8 +1908,8 @@ fn dist_remove_works() { id, None, create_asset(b"BETA", owner), - 0, - 0, + 1, + 1, 5, Some(6) )); @@ -1930,7 +1946,7 @@ fn dist_reclaim_works() { id, None, currency, - 0, + 1, AMOUNT, 5, Some(6) @@ -2002,8 +2018,8 @@ fn dist_claim_misc_bad() { id, None, create_asset(b"BETA", owner), - 0, - 0, + 1, + 1, 5, Some(6) )); @@ -2032,8 +2048,8 @@ fn dist_claim_not_targeted() { id, None, currency, - 0, - 0, + 1, + 1, 1, None, )); @@ -2206,7 +2222,7 @@ fn dist_claim_no_remaining() { // One has sufficient tokens but we'll claim from the other. // Previously, this would cause `remaining -= benefit` underflow. mk_dist(1_000_000); - let id = mk_dist(0); + let id = mk_dist(1); Timestamp::set_timestamp(5); assert_noop!(