#![allow(clippy::string_lit_as_bytes)]
#![allow(clippy::redundant_closure_call)]
#![allow(clippy::type_complexity)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod tests;
use codec::Codec;
use frame_support::{
decl_error,
decl_event,
decl_module,
decl_storage,
ensure,
storage::IterableStorageMap,
traits::{
Currency,
ExistenceRequirement,
Get,
ReservableCurrency,
},
Parameter,
};
use frame_system::{
self as system,
ensure_signed,
};
use sp_runtime::{
traits::{
AccountIdConversion,
AtLeast32Bit,
MaybeSerializeDeserialize,
Member,
Zero,
},
DispatchError,
DispatchResult,
};
use sp_std::{
fmt::Debug,
prelude::*,
};
use util::{
bank::{
BankSpend,
BankState,
OnChainTreasuryID,
SpendProposal,
SpendState,
},
traits::{
BankPermissions,
GenerateUniqueID,
GetVoteOutcome,
GroupMembership,
IDIsAvailable,
Increment,
OpenBankAccount,
OpenVote,
OrganizationSupervisorPermissions,
SeededGenerateUniqueID,
SpendGovernance,
},
vote::VoteOutcome,
};
type BalanceOf<T> = <<T as Trait>::Currency as Currency<
<T as frame_system::Trait>::AccountId,
>>::Balance;
pub trait Trait:
frame_system::Trait + org::Trait + donate::Trait + vote::Trait
{
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type SpendId: Parameter
+ Member
+ AtLeast32Bit
+ Codec
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ Debug
+ PartialOrd
+ PartialEq
+ Zero;
type Currency: Currency<Self::AccountId>
+ ReservableCurrency<Self::AccountId>;
type MaxTreasuryPerOrg: Get<u32>;
type MinimumInitialDeposit: Get<BalanceOf<Self>>;
}
decl_event!(
pub enum Event<T>
where
<T as frame_system::Trait>::AccountId,
<T as org::Trait>::OrgId,
<T as vote::Trait>::VoteId,
<T as Trait>::SpendId,
Balance = BalanceOf<T>,
{
BankAccountOpened(AccountId, OnChainTreasuryID, Balance, OrgId, Option<AccountId>),
SpendProposedByMember(AccountId, OnChainTreasuryID, SpendId, Balance, AccountId),
VoteTriggeredOnSpendProposal(AccountId, OnChainTreasuryID, SpendId, VoteId),
SudoApprovedSpendProposal(AccountId, OnChainTreasuryID, SpendId),
SpendProposalPolled(AccountId, OnChainTreasuryID, SpendId, SpendState<VoteId>),
BankAccountClosed(AccountId, OnChainTreasuryID, OrgId),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
CannotOpenBankAccountIfDepositIsBelowModuleMinimum,
CannotOpenBankAccountForOrgIfBankCountExceedsLimitPerOrg,
CannotCloseBankThatDNE,
NotPermittedToOpenBankAccountForOrg,
NotPermittedToProposeSpendForBankAccount,
NotPermittedToTriggerVoteForBankAccount,
NotPermittedToSudoApproveForBankAccount,
NotPermittedToPollSpendProposalForBankAccount,
CannotSpendIfBankDNE,
MustBeOrgSupervisorToCloseBankAccount,
CannotProposeSpendIfBankDNE,
BankMustExistToProposeSpendFrom,
CannotTriggerVoteForSpendIfBaseBankDNE,
CannotTriggerVoteForSpendIfSpendProposalDNE,
CannotTriggerVoteFromCurrentSpendProposalState,
CannotSudoApproveSpendProposalIfBaseBankDNE,
CannotSudoApproveSpendProposalIfSpendProposalDNE,
CannotApproveAlreadyApprovedSpendProposal,
CannotPollSpendProposalIfBaseBankDNE,
CannotPollSpendProposalIfSpendProposalDNE,
NoBanksForOrg,
}
}
decl_storage! {
trait Store for Module<T: Trait> as Bank {
TreasuryIDNonce get(fn treasury_id_nonce): OnChainTreasuryID;
SpendNonceMap get(fn spend_nonce_map): map
hasher(blake2_128_concat) OnChainTreasuryID => T::SpendId;
pub TotalBankCount get(fn total_bank_count): u32;
pub OrgTreasuryCount get(fn org_treasury_count): map
hasher(blake2_128_concat) T::OrgId => u32;
pub BankStores get(fn bank_stores): map
hasher(blake2_128_concat) OnChainTreasuryID =>
Option<BankState<T::AccountId, T::OrgId>>;
pub SpendProposals get(fn spend_proposals): double_map
hasher(blake2_128_concat) OnChainTreasuryID,
hasher(blake2_128_concat) T::SpendId => Option<
SpendProposal<
BalanceOf<T>,
T::AccountId,
SpendState<T::VoteId>
>
>;
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = 0]
fn open_org_bank_account(
origin,
org: T::OrgId,
deposit: BalanceOf<T>,
controller: Option<T::AccountId>,
) -> DispatchResult {
let opener = ensure_signed(origin)?;
let auth = Self::can_open_bank_account_for_org(org, &opener);
ensure!(auth, Error::<T>::NotPermittedToOpenBankAccountForOrg);
let bank_id = Self::open_bank_account(opener.clone(), org, deposit, controller.clone())?;
Self::deposit_event(RawEvent::BankAccountOpened(opener, bank_id, deposit, org, controller));
Ok(())
}
#[weight = 0]
fn member_proposes_spend(
origin,
bank_id: OnChainTreasuryID,
amount: BalanceOf<T>,
dest: T::AccountId,
) -> DispatchResult {
let caller = ensure_signed(origin)?;
let auth = Self::can_propose_spend(bank_id, &caller)?;
ensure!(auth, Error::<T>::NotPermittedToProposeSpendForBankAccount);
let new_spend_id = Self::propose_spend(bank_id, amount, dest.clone())?;
Self::deposit_event(RawEvent::SpendProposedByMember(caller, bank_id, new_spend_id.spend, amount, dest));
Ok(())
}
#[weight = 0]
fn member_triggers_vote_on_spend_proposal(
origin,
bank_id: OnChainTreasuryID,
spend_id: T::SpendId,
) -> DispatchResult {
let caller = ensure_signed(origin)?;
let auth = Self::can_trigger_vote_on_spend_proposal(bank_id, &caller)?;
ensure!(auth, Error::<T>::NotPermittedToTriggerVoteForBankAccount);
let bank_spend_id = BankSpend::new(bank_id, spend_id);
let vote_id = Self::trigger_vote_on_spend_proposal(bank_spend_id)?;
Self::deposit_event(RawEvent::VoteTriggeredOnSpendProposal(caller, bank_id, spend_id, vote_id));
Ok(())
}
#[weight = 0]
fn member_sudo_approves_spend_proposal(
origin,
bank_id: OnChainTreasuryID,
spend_id: T::SpendId,
) -> DispatchResult {
let caller = ensure_signed(origin)?;
let auth = Self::can_sudo_approve_spend_proposal(bank_id, &caller)?;
ensure!(auth, Error::<T>::NotPermittedToSudoApproveForBankAccount);
let bank_spend_id = BankSpend::new(bank_id, spend_id);
Self::sudo_approve_spend_proposal(bank_spend_id)?;
Self::deposit_event(RawEvent::SudoApprovedSpendProposal(caller, bank_id, spend_id));
Ok(())
}
#[weight = 0]
fn member_polls_spend_proposal(
origin,
bank_id: OnChainTreasuryID,
spend_id: T::SpendId,
) -> DispatchResult {
let caller = ensure_signed(origin)?;
let auth = Self::can_poll_spend_proposal(bank_id, &caller)?;
ensure!(auth, Error::<T>::NotPermittedToPollSpendProposalForBankAccount);
let bank_spend_id = BankSpend::new(bank_id, spend_id);
let state = Self::poll_spend_proposal(bank_spend_id)?;
Self::deposit_event(RawEvent::SpendProposalPolled(caller, bank_id, spend_id, state));
Ok(())
}
#[weight = 0]
fn close_org_bank_account(
origin,
bank_id: OnChainTreasuryID,
) -> DispatchResult {
let closer = ensure_signed(origin)?;
let bank = <BankStores<T>>::get(bank_id).ok_or(Error::<T>::CannotCloseBankThatDNE)?;
ensure!(
<org::Module<T>>::is_organization_supervisor(bank.org(), &closer),
Error::<T>::MustBeOrgSupervisorToCloseBankAccount
);
let bank_account_id = Self::account_id(bank_id);
let remaining_funds = <T as donate::Trait>::Currency::total_balance(&bank_account_id);
let remainder = <donate::Module<T>>::donate(
&bank_account_id,
bank.org(),
remaining_funds,
false,
)?;
<T as donate::Trait>::Currency::transfer(
&bank_account_id,
&closer,
remainder,
ExistenceRequirement::AllowDeath
)?;
<BankStores<T>>::remove(bank_id);
<OrgTreasuryCount<T>>::mutate(bank.org(), |count| *count -= 1);
<TotalBankCount>::mutate(|count| *count -= 1);
Self::deposit_event(RawEvent::BankAccountClosed(closer, bank_id, bank.org()));
Ok(())
}
}
}
impl<T: Trait> Module<T> {
pub fn is_bank(id: OnChainTreasuryID) -> bool {
!Self::id_is_available(id)
}
pub fn account_id(id: OnChainTreasuryID) -> T::AccountId {
id.into_account()
}
pub fn bank_balance(bank: OnChainTreasuryID) -> BalanceOf<T> {
<T as Trait>::Currency::total_balance(&Self::account_id(bank))
}
pub fn get_banks_for_org(
org: T::OrgId,
) -> Result<Vec<OnChainTreasuryID>, DispatchError> {
let ret_vec = <BankStores<T>>::iter()
.filter(|(_, bank_state)| bank_state.org() == org)
.map(|(bank_id, _)| bank_id)
.collect::<Vec<OnChainTreasuryID>>();
if !ret_vec.is_empty() {
Ok(ret_vec)
} else {
Err(Error::<T>::NoBanksForOrg.into())
}
}
}
impl<T: Trait> IDIsAvailable<OnChainTreasuryID> for Module<T> {
fn id_is_available(id: OnChainTreasuryID) -> bool {
<BankStores<T>>::get(id).is_none()
}
}
impl<T: Trait> IDIsAvailable<(OnChainTreasuryID, T::SpendId)> for Module<T> {
fn id_is_available(id: (OnChainTreasuryID, T::SpendId)) -> bool {
<SpendProposals<T>>::get(id.0, id.1).is_none()
}
}
impl<T: Trait> GenerateUniqueID<OnChainTreasuryID> for Module<T> {
fn generate_unique_id() -> OnChainTreasuryID {
let mut treasury_nonce_id = TreasuryIDNonce::get().increment();
while !Self::id_is_available(treasury_nonce_id) {
treasury_nonce_id = treasury_nonce_id.increment();
}
TreasuryIDNonce::put(treasury_nonce_id);
treasury_nonce_id
}
}
impl<T: Trait> SeededGenerateUniqueID<T::SpendId, OnChainTreasuryID>
for Module<T>
{
fn seeded_generate_unique_id(seed: OnChainTreasuryID) -> T::SpendId {
let mut id_nonce = <SpendNonceMap<T>>::get(seed) + 1u32.into();
while !Self::id_is_available((seed, id_nonce)) {
id_nonce += 1u32.into();
}
<SpendNonceMap<T>>::insert(seed, id_nonce);
id_nonce
}
}
impl<T: Trait> BankPermissions<OnChainTreasuryID, T::OrgId, T::AccountId>
for Module<T>
{
fn can_open_bank_account_for_org(
org: T::OrgId,
who: &T::AccountId,
) -> bool {
<org::Module<T>>::is_member_of_group(org, who)
}
fn can_propose_spend(
bank: OnChainTreasuryID,
who: &T::AccountId,
) -> Result<bool, DispatchError> {
let bank = <BankStores<T>>::get(bank)
.ok_or(Error::<T>::CannotProposeSpendIfBankDNE)?;
Ok(<org::Module<T>>::is_member_of_group(bank.org(), who))
}
fn can_trigger_vote_on_spend_proposal(
bank: OnChainTreasuryID,
who: &T::AccountId,
) -> Result<bool, DispatchError> {
let bank = <BankStores<T>>::get(bank)
.ok_or(Error::<T>::CannotTriggerVoteForSpendIfBaseBankDNE)?;
Ok(<org::Module<T>>::is_member_of_group(bank.org(), who))
}
fn can_sudo_approve_spend_proposal(
bank: OnChainTreasuryID,
who: &T::AccountId,
) -> Result<bool, DispatchError> {
let bank = <BankStores<T>>::get(bank)
.ok_or(Error::<T>::CannotSudoApproveSpendProposalIfBaseBankDNE)?;
Ok(bank.is_controller(who))
}
fn can_poll_spend_proposal(
bank: OnChainTreasuryID,
who: &T::AccountId,
) -> Result<bool, DispatchError> {
let bank = <BankStores<T>>::get(bank)
.ok_or(Error::<T>::CannotPollSpendProposalIfBaseBankDNE)?;
Ok(<org::Module<T>>::is_member_of_group(bank.org(), who))
}
fn can_spend(
bank: OnChainTreasuryID,
who: &T::AccountId,
) -> Result<bool, DispatchError> {
let bank = <BankStores<T>>::get(bank)
.ok_or(Error::<T>::CannotSpendIfBankDNE)?;
Ok(bank.is_controller(who))
}
}
impl<T: Trait> OpenBankAccount<T::OrgId, BalanceOf<T>, T::AccountId>
for Module<T>
{
type BankId = OnChainTreasuryID;
fn open_bank_account(
opener: T::AccountId,
org: T::OrgId,
deposit: BalanceOf<T>,
controller: Option<T::AccountId>,
) -> Result<Self::BankId, DispatchError> {
ensure!(
deposit >= T::MinimumInitialDeposit::get(),
Error::<T>::CannotOpenBankAccountIfDepositIsBelowModuleMinimum
);
let new_org_bank_count = <OrgTreasuryCount<T>>::get(org) + 1;
ensure!(
new_org_bank_count <= T::MaxTreasuryPerOrg::get(),
Error::<T>::CannotOpenBankAccountForOrgIfBankCountExceedsLimitPerOrg
);
let new_treasury_id = Self::generate_unique_id();
let new_bank = BankState::new(org, controller);
<T as Trait>::Currency::transfer(
&opener,
&Self::account_id(new_treasury_id),
deposit,
ExistenceRequirement::KeepAlive,
)?;
<BankStores<T>>::insert(new_treasury_id, new_bank);
<OrgTreasuryCount<T>>::insert(org, new_org_bank_count);
<TotalBankCount>::mutate(|count| *count += 1u32);
Ok(new_treasury_id)
}
}
impl<T: Trait> SpendGovernance<OnChainTreasuryID, BalanceOf<T>, T::AccountId>
for Module<T>
{
type SpendId = BankSpend<OnChainTreasuryID, T::SpendId>;
type VoteId = T::VoteId;
type SpendState = SpendState<T::VoteId>;
fn propose_spend(
bank_id: OnChainTreasuryID,
amount: BalanceOf<T>,
dest: T::AccountId,
) -> Result<Self::SpendId, DispatchError> {
ensure!(
Self::is_bank(bank_id),
Error::<T>::BankMustExistToProposeSpendFrom
);
let spend_proposal = SpendProposal::new(amount, dest);
let new_spend_id = Self::seeded_generate_unique_id(bank_id);
<SpendProposals<T>>::insert(bank_id, new_spend_id, spend_proposal);
Ok(BankSpend::new(bank_id, new_spend_id))
}
fn trigger_vote_on_spend_proposal(
spend_id: Self::SpendId,
) -> Result<Self::VoteId, DispatchError> {
let bank = <BankStores<T>>::get(spend_id.bank)
.ok_or(Error::<T>::CannotTriggerVoteForSpendIfBaseBankDNE)?;
let spend_proposal =
<SpendProposals<T>>::get(spend_id.bank, spend_id.spend).ok_or(
Error::<T>::CannotTriggerVoteForSpendIfSpendProposalDNE,
)?;
match spend_proposal.state() {
SpendState::WaitingForApproval => {
let new_vote_id = <vote::Module<T>>::open_unanimous_consent(
None,
bank.org(),
None,
)?;
let new_spend_proposal =
spend_proposal.set_state(SpendState::Voting(new_vote_id));
<SpendProposals<T>>::insert(
spend_id.bank,
spend_id.spend,
new_spend_proposal,
);
Ok(new_vote_id)
}
_ => {
Err(Error::<T>::CannotTriggerVoteFromCurrentSpendProposalState
.into())
}
}
}
fn sudo_approve_spend_proposal(spend_id: Self::SpendId) -> DispatchResult {
ensure!(
Self::is_bank(spend_id.bank),
Error::<T>::CannotSudoApproveSpendProposalIfBaseBankDNE
);
let spend_proposal =
<SpendProposals<T>>::get(spend_id.bank, spend_id.spend).ok_or(
Error::<T>::CannotSudoApproveSpendProposalIfSpendProposalDNE,
)?;
match spend_proposal.state() {
SpendState::WaitingForApproval | SpendState::Voting(_) => {
let new_spend_proposal = if let Ok(()) =
<T as Trait>::Currency::transfer(
&Self::account_id(spend_id.bank),
&spend_proposal.dest(),
spend_proposal.amount(),
ExistenceRequirement::KeepAlive,
) {
spend_proposal.set_state(SpendState::ApprovedAndExecuted)
} else {
spend_proposal.set_state(SpendState::ApprovedButNotExecuted)
};
<SpendProposals<T>>::insert(
spend_id.bank,
spend_id.spend,
new_spend_proposal,
);
Ok(())
}
_ => {
Err(Error::<T>::CannotApproveAlreadyApprovedSpendProposal
.into())
}
}
}
fn poll_spend_proposal(
spend_id: Self::SpendId,
) -> Result<Self::SpendState, DispatchError> {
ensure!(
Self::is_bank(spend_id.bank),
Error::<T>::CannotPollSpendProposalIfBaseBankDNE
);
let spend_proposal =
<SpendProposals<T>>::get(spend_id.bank, spend_id.spend)
.ok_or(Error::<T>::CannotPollSpendProposalIfSpendProposalDNE)?;
match spend_proposal.state() {
SpendState::Voting(vote_id) => {
let vote_outcome =
<vote::Module<T>>::get_vote_outcome(vote_id)?;
if vote_outcome == VoteOutcome::Approved {
let new_spend_proposal = if let Ok(()) =
<T as Trait>::Currency::transfer(
&Self::account_id(spend_id.bank),
&spend_proposal.dest(),
spend_proposal.amount(),
ExistenceRequirement::KeepAlive,
) {
spend_proposal
.set_state(SpendState::ApprovedAndExecuted)
} else {
spend_proposal
.set_state(SpendState::ApprovedButNotExecuted)
};
let ret_state = new_spend_proposal.state();
<SpendProposals<T>>::insert(
spend_id.bank,
spend_id.spend,
new_spend_proposal,
);
Ok(ret_state)
} else {
Ok(spend_proposal.state())
}
}
_ => Ok(spend_proposal.state()),
}
}
}