#![allow(clippy::string_lit_as_bytes)]
#![allow(clippy::redundant_closure_call)]
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
#![allow(non_snake_case)]
#![cfg_attr(not(feature = "std"), no_std)]
mod tests;
use frame_support::{
decl_error, decl_event, decl_module, decl_storage, ensure,
traits::{Currency, Get},
};
use frame_system::{self as system, ensure_signed};
use sp_runtime::{DispatchError, DispatchResult, Permill};
use sp_std::prelude::*;
use util::{
bank::{BankMapID, OnChainTreasuryID, WithdrawalPermissions},
bounty::{
ApplicationState, BountyInformation, BountyMapID, GrantApplication, MilestoneStatus,
MilestoneSubmission, ReviewBoard, TeamID, VoteID,
},
organization::{ShareID, TermsOfAgreement},
traits::{
ApplyVote, ApproveGrant, ApproveWithoutTransfer, Approved, BankDepositsAndSpends,
BankReservations, BankSpends, BankStorageInfo, CheckBankBalances, CheckVoteStatus,
CommitAndTransfer, CreateBounty, DepositIntoBank, FoundationParts, GenerateUniqueID,
GetInnerOuterShareGroups, GetVoteOutcome, IDIsAvailable, MintableSignal, OnChainBank,
OpenPetition, OpenShareGroupVote, OrgChecks, OrganizationDNS,
OwnershipProportionCalculations, RegisterBankAccount, RegisterFoundation,
RegisterShareGroup, SeededGenerateUniqueID, SetMakeTransfer, ShareGroupChecks,
SignPetition, SpendApprovedGrant, StartReview, StartTeamConsentPetition,
SubmitGrantApplication, SubmitMilestone, SuperviseGrantApplication, SupervisorPermissions,
TermSheetExit, ThresholdVote, UpdatePetition, UseTermsOfAgreement, VoteOnProposal,
WeightedShareIssuanceWrapper, WeightedShareWrapper,
},
voteyesno::{SupportedVoteTypes, ThresholdConfig},
};
pub type IpfsReference = Vec<u8>;
pub type OrgId = u32;
pub type BountyId = u32;
pub type SharesOf<T> = <<T as Trait>::Organization as WeightedShareWrapper<
u32,
u32,
<T as frame_system::Trait>::AccountId,
>>::Shares;
pub type BalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
pub type SignalOf<T> = <<T as Trait>::VoteYesNo as ThresholdVote>::Signal;
pub trait Trait: frame_system::Trait {
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type Currency: Currency<Self::AccountId>;
type Organization: OrgChecks<u32, Self::AccountId>
+ ShareGroupChecks<u32, ShareID, Self::AccountId>
+ GetInnerOuterShareGroups<u32, ShareID, Self::AccountId>
+ SupervisorPermissions<u32, ShareID, Self::AccountId>
+ WeightedShareWrapper<u32, u32, Self::AccountId>
+ WeightedShareIssuanceWrapper<u32, u32, Self::AccountId, Permill>
+ RegisterShareGroup<u32, ShareID, Self::AccountId, SharesOf<Self>>
+ OrganizationDNS<u32, Self::AccountId, IpfsReference>;
type Bank: IDIsAvailable<OnChainTreasuryID>
+ IDIsAvailable<(OnChainTreasuryID, BankMapID, u32)>
+ GenerateUniqueID<OnChainTreasuryID>
+ OnChainBank
+ RegisterBankAccount<
Self::AccountId,
WithdrawalPermissions<Self::AccountId>,
BalanceOf<Self>,
> + OwnershipProportionCalculations<
Self::AccountId,
WithdrawalPermissions<Self::AccountId>,
BalanceOf<Self>,
Permill,
> + BankDepositsAndSpends<BalanceOf<Self>>
+ CheckBankBalances<BalanceOf<Self>>
+ DepositIntoBank<
Self::AccountId,
WithdrawalPermissions<Self::AccountId>,
IpfsReference,
BalanceOf<Self>,
> + BankReservations<
Self::AccountId,
WithdrawalPermissions<Self::AccountId>,
BalanceOf<Self>,
IpfsReference,
> + BankSpends<Self::AccountId, WithdrawalPermissions<Self::AccountId>, BalanceOf<Self>>
+ CommitAndTransfer<
Self::AccountId,
WithdrawalPermissions<Self::AccountId>,
BalanceOf<Self>,
IpfsReference,
> + BankStorageInfo<Self::AccountId, WithdrawalPermissions<Self::AccountId>, BalanceOf<Self>>
+ TermSheetExit<Self::AccountId, BalanceOf<Self>>;
type VotePetition: IDIsAvailable<u32>
+ GenerateUniqueID<u32>
+ GetVoteOutcome
+ OpenPetition<IpfsReference, Self::BlockNumber>
+ SignPetition<Self::AccountId, IpfsReference>
+ UpdatePetition<Self::AccountId, IpfsReference>;
type VoteYesNo: IDIsAvailable<u32>
+ GenerateUniqueID<u32>
+ MintableSignal<Self::AccountId, Self::BlockNumber, Permill>
+ GetVoteOutcome
+ ThresholdVote
+ OpenShareGroupVote<Self::AccountId, Self::BlockNumber, Permill>
+ ApplyVote
+ CheckVoteStatus
+ VoteOnProposal<Self::AccountId, IpfsReference, Self::BlockNumber, Permill>;
type MinimumBountyCollateralRatio: Get<Permill>;
type BountyLowerBound: Get<BalanceOf<Self>>;
}
decl_event!(
pub enum Event<T>
where
<T as frame_system::Trait>::AccountId,
Currency = BalanceOf<T>,
AppState = ApplicationState<<T as frame_system::Trait>::AccountId>,
{
FoundationRegisteredFromOnChainBank(OrgId, OnChainTreasuryID),
FoundationPostedBounty(AccountId, OrgId, BountyId, OnChainTreasuryID, IpfsReference, Currency, Currency),
GrantApplicationSubmittedForBounty(AccountId, BountyId, u32, IpfsReference, Currency),
ApplicationReviewTriggered(AccountId, u32, u32, AppState),
SudoApprovedApplication(AccountId, u32, u32, AppState),
ApplicationPolled(u32, u32, AppState),
MilestoneSubmitted(AccountId, BountyId, u32, u32),
MilestoneReviewTriggered(AccountId, BountyId, u32, MilestoneStatus),
SudoApprovedMilestone(AccountId, BountyId, u32, MilestoneStatus),
MilestonePolled(AccountId, BountyId, u32, MilestoneStatus),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
NoBankExistsAtInputTreasuryIdForCreatingBounty,
WithdrawalPermissionsOfBankMustAlignWithCallerToUseForBounty,
OrganizationBankDoesNotHaveEnoughBalanceToCreateBounty,
MinimumBountyClaimedAmountMustMeetModuleLowerBound,
BountyCollateralRatioMustMeetModuleRequirements,
FoundationMustBeRegisteredToCreateBounty,
CannotRegisterFoundationFromOrgBankRelationshipThatDNE,
GrantApplicationFailsIfBountyDNE,
GrantRequestExceedsAvailableBountyFunds,
CannotReviewApplicationIfBountyDNE,
CannotReviewApplicationIfApplicationDNE,
CannotPollApplicationIfBountyDNE,
CannotPollApplicationIfApplicationDNE,
CannotSudoApproveIfBountyDNE,
CannotSudoApproveAppIfNotAssignedSudo,
CannotSudoApproveIfGrantAppDNE,
CannotSubmitMilestoneIfApplicationDNE,
CannotTriggerMilestoneReviewIfBountyDNE,
CannotTriggerMilestoneReviewUnlessMember,
CannotSudoApproveMilestoneIfNotAssignedSudo,
CannotSudoApproveMilestoneIfMilestoneSubmissionDNE,
CallerMustBeMemberOfFlatShareGroupToSubmitMilestones,
CannotTriggerMilestoneReviewIfMilestoneSubmissionDNE,
CannotPollMilestoneReviewIfBountyDNE,
CannotPollMilestoneReviewUnlessMember,
CannotPollMilestoneIfMilestoneSubmissionDNE,
CannotPollMilestoneIfReferenceApplicationDNE,
SubmissionIsNotReadyForReview,
AppStateCannotBeSudoApprovedForAGrantFromCurrentState,
ApplicationMustBeSubmittedAwaitingResponseToTriggerReview,
ApplicationMustApprovedAndLiveWithTeamIDMatchingInput,
MilestoneSubmissionRequestExceedsApprovedApplicationsLimit,
AccountNotAuthorizedToTriggerApplicationReview,
ReviewBoardWeightedShapeDoesntSupportPetitionReview,
ReviewBoardFlatShapeDoesntSupportThresholdReview,
ApplicationMustBeUnderReviewToPoll,
}
}
decl_storage! {
trait Store for Module<T: Trait> as Court {
BountyNonce get(fn bounty_nonce): BountyId;
BountyAssociatedNonces get(fn bounty_associated_nonces): double_map
hasher(opaque_blake2_256) BountyId,
hasher(opaque_blake2_256) BountyMapID => u32;
RegisteredFoundations get(fn registered_foundations): double_map
hasher(blake2_128_concat) OrgId,
hasher(blake2_128_concat) OnChainTreasuryID => bool;
RegisteredTeams get(fn registered_teams): map
hasher(blake2_128_concat) TeamID<T::AccountId> => bool;
FoundationSponsoredBounties get(fn foundation_sponsored_bounties): map
hasher(opaque_blake2_256) BountyId => Option<
BountyInformation<T::AccountId, IpfsReference, ThresholdConfig<SignalOf<T>, Permill>, BalanceOf<T>>
>;
BountyApplications get(fn bounty_applications): double_map
hasher(opaque_blake2_256) BountyId,
hasher(opaque_blake2_256) u32 => Option<GrantApplication<T::AccountId, SharesOf<T>, BalanceOf<T>, IpfsReference>>;
MilestoneSubmissions get(fn milestone_submissions): double_map
hasher(opaque_blake2_256) BountyId,
hasher(opaque_blake2_256) u32 => Option<MilestoneSubmission<IpfsReference, BalanceOf<T>, T::AccountId>>;
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = 0]
pub fn direct__register_foundation_from_existing_bank(
origin,
registered_organization: OrgId,
bank_account: OnChainTreasuryID,
) -> DispatchResult {
let _ = ensure_signed(origin)?;
Self::register_foundation_from_existing_bank(registered_organization, bank_account)?;
Self::deposit_event(RawEvent::FoundationRegisteredFromOnChainBank(registered_organization, bank_account));
Ok(())
}
#[weight = 0]
pub fn direct__create_bounty(
origin,
registered_organization: OrgId,
description: IpfsReference,
bank_account: OnChainTreasuryID,
amount_reserved_for_bounty: BalanceOf<T>,
amount_claimed_available: BalanceOf<T>,
acceptance_committee: ReviewBoard<T::AccountId, IpfsReference, ThresholdConfig<SignalOf<T>, Permill>>,
supervision_committee: Option<ReviewBoard<T::AccountId, IpfsReference, ThresholdConfig<SignalOf<T>, Permill>>>,
) -> DispatchResult {
let bounty_creator = ensure_signed(origin)?;
let bounty_identifier = Self::create_bounty(
registered_organization,
bounty_creator.clone(),
bank_account,
description.clone(),
amount_reserved_for_bounty,
amount_claimed_available,
acceptance_committee,
supervision_committee,
)?;
Self::deposit_event(RawEvent::FoundationPostedBounty(
bounty_creator,
registered_organization,
bounty_identifier,
bank_account,
description,
amount_reserved_for_bounty,
amount_claimed_available,
));
Ok(())
}
#[weight = 0]
pub fn direct__submit_grant_application(
origin,
bounty_id: BountyId,
description: IpfsReference,
total_amount: BalanceOf<T>,
terms_of_agreement: TermsOfAgreement<T::AccountId, SharesOf<T>>,
) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let new_grant_app_id = Self::submit_grant_application(submitter.clone(), bounty_id, description.clone(), total_amount, terms_of_agreement)?;
Self::deposit_event(RawEvent::GrantApplicationSubmittedForBounty(submitter, bounty_id, new_grant_app_id, description, total_amount));
Ok(())
}
#[weight = 0]
pub fn direct__trigger_application_review(
origin,
bounty_id: BountyId,
application_id: u32,
) -> DispatchResult {
let trigger = ensure_signed(origin)?;
let application_state = Self::trigger_application_review(trigger.clone(), bounty_id, application_id)?;
Self::deposit_event(RawEvent::ApplicationReviewTriggered(trigger, bounty_id, application_id, application_state));
Ok(())
}
#[weight = 0]
pub fn direct__sudo_approve_application(
origin,
bounty_id: BountyId,
application_id: u32,
) -> DispatchResult {
let purported_sudo = ensure_signed(origin)?;
let app_state = Self::sudo_approve_application(purported_sudo.clone(), bounty_id, application_id)?;
Self::deposit_event(RawEvent::SudoApprovedApplication(purported_sudo, bounty_id, application_id, app_state));
Ok(())
}
#[weight = 0]
fn any_acc__poll_application(
origin,
bounty_id: BountyId,
application_id: u32,
) -> DispatchResult {
let _ = ensure_signed(origin)?;
let app_state = Self::poll_application(bounty_id, application_id)?;
Self::deposit_event(RawEvent::ApplicationPolled(bounty_id, application_id, app_state));
Ok(())
}
#[weight = 0]
fn direct__submit_milestone(
origin,
bounty_id: BountyId,
application_id: u32,
team_id: TeamID<T::AccountId>,
submission_reference: IpfsReference,
amount_requested: BalanceOf<T>,
) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let new_milestone_id = Self::submit_milestone(submitter.clone(), bounty_id, application_id, team_id, submission_reference, amount_requested)?;
Self::deposit_event(RawEvent::MilestoneSubmitted(submitter, bounty_id, application_id, new_milestone_id));
Ok(())
}
#[weight = 0]
fn direct__trigger_milestone_review(
origin,
bounty_id: BountyId,
milestone_id: u32,
) -> DispatchResult {
let trigger = ensure_signed(origin)?;
let milestone_state = Self::trigger_milestone_review(trigger.clone(), bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::MilestoneReviewTriggered(trigger, bounty_id, milestone_id, milestone_state));
Ok(())
}
#[weight = 0]
fn direct__sudo_approves_milestone(
origin,
bounty_id: BountyId,
milestone_id: u32,
) -> DispatchResult {
let purported_sudo = ensure_signed(origin)?;
let milestone_state = Self::sudo_approves_milestone(purported_sudo.clone(), bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::SudoApprovedMilestone(purported_sudo, bounty_id, milestone_id, milestone_state));
Ok(())
}
#[weight = 0]
fn direct__poll_milestone(
origin,
bounty_id: BountyId,
milestone_id: u32,
) -> DispatchResult {
let poller = ensure_signed(origin)?;
let milestone_state = Self::poll_milestone(poller.clone(), bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::MilestonePolled(poller, bounty_id, milestone_id, milestone_state));
Ok(())
}
}
}
impl<T: Trait> Module<T> {
fn collateral_satisfies_module_limits(collateral: BalanceOf<T>, claimed: BalanceOf<T>) -> bool {
let ratio = Permill::from_rational_approximation(collateral, claimed);
ratio >= T::MinimumBountyCollateralRatio::get()
}
fn account_can_trigger_review(
account: &T::AccountId,
acceptance_committee: ReviewBoard<
T::AccountId,
IpfsReference,
ThresholdConfig<SignalOf<T>, Permill>,
>,
) -> bool {
match acceptance_committee {
ReviewBoard::FlatPetitionReview(_, org_id, share_id, _, _, _) => {
<<T as Trait>::Organization as ShareGroupChecks<
u32,
ShareID,
T::AccountId,
>>::check_membership_in_share_group(org_id, ShareID::Flat(share_id), account)
},
ReviewBoard::WeightedThresholdReview(_, org_id, share_id, _, _) => {
<<T as Trait>::Organization as ShareGroupChecks<
u32,
ShareID,
T::AccountId,
>>::check_membership_in_share_group(org_id, ShareID::WeightedAtomic(share_id), account)
},
}
}
pub fn account_can_submit_milestone_for_team(
account: &T::AccountId,
team_id: TeamID<T::AccountId>,
) -> bool {
<<T as Trait>::Organization as ShareGroupChecks<
u32,
ShareID,
T::AccountId,
>>::check_membership_in_share_group(team_id.org(), ShareID::Flat(team_id.flat_share_id()), account)
}
fn check_vote_status(vote_id: VoteID) -> Result<bool, DispatchError> {
match vote_id {
VoteID::Petition(petition_id) => {
let outcome = <<T as Trait>::VotePetition as GetVoteOutcome>::get_vote_outcome(
petition_id.into(),
)?;
Ok(outcome.approved())
}
VoteID::Threshold(threshold_id) => {
let outcome = <<T as Trait>::VoteYesNo as GetVoteOutcome>::get_vote_outcome(
threshold_id.into(),
)?;
Ok(outcome.approved())
}
}
}
fn dispatch_threshold_review(
organization: u32,
weighted_share_id: u32,
vote_type: SupportedVoteTypes,
threshold: ThresholdConfig<SignalOf<T>, Permill>,
duration: Option<T::BlockNumber>,
) -> Result<VoteID, DispatchError> {
let id: u32 = <<T as Trait>::VoteYesNo as OpenShareGroupVote<
T::AccountId,
T::BlockNumber,
Permill,
>>::open_share_group_vote(
organization,
weighted_share_id,
vote_type.into(),
threshold.into(),
duration,
)?
.into();
Ok(VoteID::Threshold(id))
}
fn dispatch_unanimous_petition_review(
organization: u32,
flat_share_id: u32,
topic: Option<IpfsReference>,
duration: Option<T::BlockNumber>,
) -> Result<VoteID, DispatchError> {
let id: u32 = <<T as Trait>::VotePetition as OpenPetition<
IpfsReference,
T::BlockNumber,
>>::open_unanimous_approval_petition(
organization, flat_share_id, topic, duration
)?
.into();
Ok(VoteID::Petition(id))
}
fn dispatch_petition_review(
organization: u32,
flat_share_id: u32,
topic: Option<IpfsReference>,
required_support: u32,
required_against: Option<u32>,
duration: Option<T::BlockNumber>,
) -> Result<VoteID, DispatchError> {
let id: u32 = <<T as Trait>::VotePetition as OpenPetition<
IpfsReference,
T::BlockNumber,
>>::open_petition(
organization,
flat_share_id,
topic,
required_support,
required_against,
duration,
)?
.into();
Ok(VoteID::Petition(id))
}
}
impl<T: Trait> IDIsAvailable<BountyId> for Module<T> {
fn id_is_available(id: BountyId) -> bool {
<FoundationSponsoredBounties<T>>::get(id).is_none()
}
}
impl<T: Trait> IDIsAvailable<(BountyId, BountyMapID, u32)> for Module<T> {
fn id_is_available(id: (BountyId, BountyMapID, u32)) -> bool {
match id.1 {
BountyMapID::ApplicationId => <BountyApplications<T>>::get(id.0, id.2).is_none(),
BountyMapID::MilestoneId => <MilestoneSubmissions<T>>::get(id.0, id.2).is_none(),
}
}
}
impl<T: Trait> SeededGenerateUniqueID<u32, (BountyId, BountyMapID)> for Module<T> {
fn seeded_generate_unique_id(seed: (BountyId, BountyMapID)) -> u32 {
let mut new_id = <BountyAssociatedNonces>::get(seed.0, seed.1) + 1u32;
while !Self::id_is_available((seed.0, seed.1, new_id)) {
new_id += 1u32;
}
<BountyAssociatedNonces>::insert(seed.0, seed.1, new_id);
new_id
}
}
impl<T: Trait> GenerateUniqueID<BountyId> for Module<T> {
fn generate_unique_id() -> BountyId {
let mut id_counter = BountyNonce::get() + 1;
while !Self::id_is_available(id_counter) {
id_counter += 1;
}
BountyNonce::put(id_counter);
id_counter
}
}
impl<T: Trait> FoundationParts for Module<T> {
type OrgId = OrgId;
type BountyId = BountyId;
type BankId = OnChainTreasuryID;
type TeamId = TeamID<T::AccountId>;
type MultiShareId = ShareID;
type MultiVoteId = VoteID;
}
impl<T: Trait> RegisterFoundation<BalanceOf<T>, T::AccountId> for Module<T> {
fn register_foundation_from_deposit(
_from: T::AccountId,
_for_org: Self::OrgId,
_amount: BalanceOf<T>,
) -> Result<Self::BankId, DispatchError> {
todo!()
}
fn register_foundation_from_existing_bank(
org: Self::OrgId,
bank: Self::BankId,
) -> DispatchResult {
ensure!(
<<T as Trait>::Bank as RegisterBankAccount<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
>>::check_bank_owner(bank.into(), org.into()),
Error::<T>::CannotRegisterFoundationFromOrgBankRelationshipThatDNE
);
RegisteredFoundations::insert(org, bank, true);
Ok(())
}
}
impl<T: Trait> CreateBounty<BalanceOf<T>, T::AccountId, IpfsReference> for Module<T> {
type BountyInfo = BountyInformation<
T::AccountId,
IpfsReference,
ThresholdConfig<SignalOf<T>, Permill>,
BalanceOf<T>,
>;
type ReviewCommittee =
ReviewBoard<T::AccountId, IpfsReference, ThresholdConfig<SignalOf<T>, Permill>>;
fn screen_bounty_creation(
foundation: u32,
caller: T::AccountId,
bank_account: Self::BankId,
description: IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
amount_claimed_available: BalanceOf<T>,
acceptance_committee: Self::ReviewCommittee,
supervision_committee: Option<Self::ReviewCommittee>,
) -> Result<Self::BountyInfo, DispatchError> {
ensure!(
RegisteredFoundations::get(foundation, bank_account),
Error::<T>::FoundationMustBeRegisteredToCreateBounty
);
ensure!(
amount_claimed_available >= T::BountyLowerBound::get(),
Error::<T>::MinimumBountyClaimedAmountMustMeetModuleLowerBound
);
ensure!(
Self::collateral_satisfies_module_limits(
amount_reserved_for_bounty,
amount_claimed_available,
),
Error::<T>::BountyCollateralRatioMustMeetModuleRequirements
);
let spend_reservation_id = <<T as Trait>::Bank as BankReservations<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
IpfsReference,
>>::reserve_for_spend(
caller,
bank_account.into(),
description.clone(),
amount_reserved_for_bounty,
acceptance_committee.clone().into(),
)?;
let new_bounty_info = BountyInformation::new(
description,
foundation,
bank_account,
spend_reservation_id,
amount_reserved_for_bounty,
amount_claimed_available,
acceptance_committee,
supervision_committee,
);
Ok(new_bounty_info)
}
fn create_bounty(
foundation: u32,
caller: T::AccountId,
bank_account: Self::BankId,
description: IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
amount_claimed_available: BalanceOf<T>,
acceptance_committee: Self::ReviewCommittee,
supervision_committee: Option<Self::ReviewCommittee>,
) -> Result<u32, DispatchError> {
ensure!(
<<T as Trait>::Organization as OrgChecks<u32, <T as frame_system::Trait>::AccountId>>::check_org_existence(foundation),
Error::<T>::NoBankExistsAtInputTreasuryIdForCreatingBounty
);
let new_bounty_info = Self::screen_bounty_creation(
foundation,
caller,
bank_account,
description,
amount_reserved_for_bounty,
amount_claimed_available,
acceptance_committee,
supervision_committee,
)?;
let new_bounty_id = Self::generate_unique_id();
<FoundationSponsoredBounties<T>>::insert(new_bounty_id, new_bounty_info);
Ok(new_bounty_id)
}
}
impl<T: Trait> SubmitGrantApplication<BalanceOf<T>, T::AccountId, IpfsReference> for Module<T> {
type GrantApp = GrantApplication<T::AccountId, SharesOf<T>, BalanceOf<T>, IpfsReference>;
fn form_grant_application(
caller: T::AccountId,
bounty_id: u32,
description: IpfsReference,
total_amount: BalanceOf<T>,
terms_of_agreement: Self::TermsOfAgreement,
) -> Result<Self::GrantApp, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::GrantApplicationFailsIfBountyDNE)?;
ensure!(
bounty_info.claimed_funding_available() >= total_amount,
Error::<T>::GrantRequestExceedsAvailableBountyFunds
);
let grant_app =
GrantApplication::new(caller, description, total_amount, terms_of_agreement);
Ok(grant_app)
}
fn submit_grant_application(
caller: T::AccountId,
bounty_id: u32,
description: IpfsReference,
total_amount: BalanceOf<T>,
terms_of_agreement: Self::TermsOfAgreement,
) -> Result<u32, DispatchError> {
let formed_grant_app = Self::form_grant_application(
caller,
bounty_id,
description,
total_amount,
terms_of_agreement,
)?;
let new_application_id =
Self::seeded_generate_unique_id((bounty_id, BountyMapID::ApplicationId));
<BountyApplications<T>>::insert(bounty_id, new_application_id, formed_grant_app);
Ok(new_application_id)
}
}
impl<T: Trait> UseTermsOfAgreement<T::AccountId> for Module<T> {
type TermsOfAgreement = TermsOfAgreement<T::AccountId, SharesOf<T>>;
fn request_consent_on_terms_of_agreement(
bounty_org: u32,
terms: TermsOfAgreement<T::AccountId, SharesOf<T>>,
) -> Result<(ShareID, VoteID), DispatchError> {
let outer_flat_share_id_for_team =
<<T as Trait>::Organization as RegisterShareGroup<
u32,
ShareID,
T::AccountId,
SharesOf<T>,
>>::register_outer_flat_share_group(bounty_org, terms.flat())?;
let rewrapped_share_id: ShareID = outer_flat_share_id_for_team;
let unwrapped_share_id: u32 = outer_flat_share_id_for_team.into();
let vote_id =
Self::dispatch_unanimous_petition_review(bounty_org, unwrapped_share_id, None, None)?;
Ok((rewrapped_share_id, vote_id))
}
fn approve_grant_to_register_team(
bounty_org: u32,
flat_share_id: u32,
terms: Self::TermsOfAgreement,
) -> Result<Self::TeamId, DispatchError> {
let weighted_share_id =
<<T as Trait>::Organization as RegisterShareGroup<
u32,
ShareID,
T::AccountId,
SharesOf<T>,
>>::register_outer_weighted_share_group(bounty_org, terms.weighted())?;
let new_team = TeamID::new(
bounty_org,
terms.supervisor(),
flat_share_id,
weighted_share_id.into(),
);
<RegisteredTeams<T>>::insert(new_team.clone(), true);
Ok(new_team)
}
}
impl<T: Trait> SuperviseGrantApplication<BalanceOf<T>, T::AccountId, IpfsReference> for Module<T> {
type AppState = ApplicationState<T::AccountId>;
fn trigger_application_review(
trigger: T::AccountId,
bounty_id: u32,
application_id: u32,
) -> Result<Self::AppState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotReviewApplicationIfBountyDNE)?;
let application_to_review = <BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotReviewApplicationIfApplicationDNE)?;
ensure!(
application_to_review.state() == ApplicationState::SubmittedAwaitingResponse,
Error::<T>::ApplicationMustBeSubmittedAwaitingResponseToTriggerReview
);
ensure!(
Self::account_can_trigger_review(&trigger, bounty_info.acceptance_committee()),
Error::<T>::AccountNotAuthorizedToTriggerApplicationReview
);
let new_vote_id = match bounty_info.acceptance_committee() {
ReviewBoard::FlatPetitionReview(
_,
org_id,
flat_share_id,
required_support,
required_against,
_,
) => {
Self::dispatch_petition_review(
org_id,
flat_share_id,
None,
required_support,
required_against,
None,
)?
}
ReviewBoard::WeightedThresholdReview(
_,
org_id,
weighted_share_id,
vote_type,
threshold,
) => Self::dispatch_threshold_review(
org_id,
weighted_share_id,
vote_type,
threshold,
None,
)?,
};
let new_application = application_to_review.start_review(new_vote_id);
let app_state = new_application.state();
<BountyApplications<T>>::insert(bounty_id, application_id, new_application);
Ok(app_state)
}
fn sudo_approve_application(
caller: T::AccountId,
bounty_id: u32,
application_id: u32,
) -> Result<Self::AppState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotSudoApproveIfBountyDNE)?;
ensure!(
bounty_info.acceptance_committee().is_sudo(&caller),
Error::<T>::CannotSudoApproveAppIfNotAssignedSudo
);
let app = <BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotSudoApproveIfGrantAppDNE)?;
ensure!(
app.state().live(),
Error::<T>::AppStateCannotBeSudoApprovedForAGrantFromCurrentState
);
let (team_flat_share_id, team_consent_vote_id) =
Self::request_consent_on_terms_of_agreement(
bounty_info.foundation(),
app.terms_of_agreement(),
)?;
let new_application =
app.start_team_consent_petition(team_flat_share_id, team_consent_vote_id);
let ret_state = new_application.state();
<BountyApplications<T>>::insert(bounty_id, application_id, new_application);
Ok(ret_state)
}
fn poll_application(
bounty_id: u32,
application_id: u32,
) -> Result<Self::AppState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotPollApplicationIfBountyDNE)?;
let application_under_review = <BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotPollApplicationIfApplicationDNE)?;
match application_under_review.state() {
ApplicationState::UnderReviewByAcceptanceCommittee(wrapped_vote_id) => {
let status = Self::check_vote_status(wrapped_vote_id)?;
if status {
let (team_flat_share_id, team_consent_vote_id) =
Self::request_consent_on_terms_of_agreement(
bounty_info.foundation(),
application_under_review.terms_of_agreement(),
)?;
let new_application = application_under_review
.start_team_consent_petition(team_flat_share_id, team_consent_vote_id);
let new_state = new_application.state();
<BountyApplications<T>>::insert(bounty_id, application_id, new_application);
Ok(new_state)
} else {
Ok(application_under_review.state())
}
}
ApplicationState::ApprovedByFoundationAwaitingTeamConsent(
wrapped_share_id,
wrapped_vote_id,
) => {
let status = Self::check_vote_status(wrapped_vote_id)?;
if status {
let newly_registered_team_id = Self::approve_grant_to_register_team(
bounty_info.foundation(),
wrapped_share_id.into(),
application_under_review.terms_of_agreement(),
)?;
let new_application =
application_under_review.approve_grant(newly_registered_team_id);
let new_state = new_application.state();
<BountyApplications<T>>::insert(bounty_id, application_id, new_application);
Ok(new_state)
} else {
Ok(application_under_review.state())
}
}
_ => Ok(application_under_review.state()),
}
}
}
impl<T: Trait> SubmitMilestone<BalanceOf<T>, T::AccountId, IpfsReference> for Module<T> {
type Milestone = MilestoneSubmission<IpfsReference, BalanceOf<T>, T::AccountId>;
type MilestoneState = MilestoneStatus;
fn submit_milestone(
caller: T::AccountId,
bounty_id: u32,
application_id: u32,
team_id: Self::TeamId,
submission_reference: IpfsReference,
amount_requested: BalanceOf<T>,
) -> Result<u32, DispatchError> {
let application_to_review = <BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotSubmitMilestoneIfApplicationDNE)?;
ensure!(
application_to_review
.state()
.matches_registered_team(team_id.clone()),
Error::<T>::ApplicationMustApprovedAndLiveWithTeamIDMatchingInput
);
ensure!(
application_to_review.total_amount() >= amount_requested,
Error::<T>::MilestoneSubmissionRequestExceedsApprovedApplicationsLimit
);
ensure!(
Self::account_can_submit_milestone_for_team(&caller, team_id.clone()),
Error::<T>::CallerMustBeMemberOfFlatShareGroupToSubmitMilestones,
);
let new_milestone = MilestoneSubmission::new(
caller,
application_id,
team_id,
submission_reference,
amount_requested,
);
let new_milestone_id =
Self::seeded_generate_unique_id((bounty_id, BountyMapID::MilestoneId));
<MilestoneSubmissions<T>>::insert(bounty_id, new_milestone_id, new_milestone);
Ok(new_milestone_id)
}
fn trigger_milestone_review(
caller: T::AccountId,
bounty_id: u32,
milestone_id: u32,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotTriggerMilestoneReviewIfBountyDNE)?;
let milestone_submission = <MilestoneSubmissions<T>>::get(bounty_id, milestone_id)
.ok_or(Error::<T>::CannotTriggerMilestoneReviewIfMilestoneSubmissionDNE)?;
let milestone_review_board =
if let Some(separate_board) = bounty_info.supervision_committee() {
separate_board
} else {
bounty_info.acceptance_committee()
};
ensure!(
Self::account_can_trigger_review(&caller, milestone_review_board.clone()),
Error::<T>::CannotTriggerMilestoneReviewUnlessMember
);
ensure!(
milestone_submission.ready_for_review(),
Error::<T>::SubmissionIsNotReadyForReview
);
<<T as Trait>::Bank as BankReservations<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
IpfsReference,
>>::commit_reserved_spend_for_transfer(
caller,
bounty_info.bank_account().into(),
bounty_info.spend_reservation(),
milestone_submission.submission(),
milestone_submission.amount(),
milestone_submission.team().into(),
)?;
let new_vote_id = match milestone_review_board {
ReviewBoard::FlatPetitionReview(
_,
org_id,
flat_share_id,
required_support,
required_against,
_,
) => {
Self::dispatch_petition_review(
org_id,
flat_share_id,
None,
required_support,
required_against,
None,
)?
}
ReviewBoard::WeightedThresholdReview(
_,
org_id,
weighted_share_id,
vote_type,
threshold,
) => Self::dispatch_threshold_review(
org_id,
weighted_share_id,
vote_type,
threshold,
None,
)?,
};
let new_milestone_submission = milestone_submission.start_review(new_vote_id);
let milestone_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(bounty_id, milestone_id, new_milestone_submission);
Ok(milestone_state)
}
fn sudo_approves_milestone(
caller: T::AccountId,
bounty_id: u32,
milestone_id: u32,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotTriggerMilestoneReviewIfBountyDNE)?;
let milestone_review_board =
if let Some(separate_board) = bounty_info.supervision_committee() {
separate_board
} else {
bounty_info.acceptance_committee()
};
ensure!(
milestone_review_board.is_sudo(&caller),
Error::<T>::CannotSudoApproveMilestoneIfNotAssignedSudo
);
let milestone_submission = <MilestoneSubmissions<T>>::get(bounty_id, milestone_id)
.ok_or(Error::<T>::CannotSudoApproveMilestoneIfMilestoneSubmissionDNE)?;
ensure!(
milestone_submission.ready_for_review(),
Error::<T>::SubmissionIsNotReadyForReview
);
let new_transfer_id = <<T as Trait>::Bank as CommitAndTransfer<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
IpfsReference,
>>::commit_and_transfer_spending_power(
caller,
bounty_info.bank_account().into(),
bounty_info.spend_reservation(),
milestone_submission.submission(),
milestone_submission.amount(),
milestone_submission.team().into(),
)?;
let new_milestone_submission =
milestone_submission.set_make_transfer(bounty_info.bank_account(), new_transfer_id);
let new_milestone_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(bounty_id, milestone_id, new_milestone_submission);
Ok(new_milestone_state)
}
fn poll_milestone(
caller: T::AccountId,
bounty_id: u32,
milestone_id: u32,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty_info = <FoundationSponsoredBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotPollMilestoneReviewIfBountyDNE)?;
let milestone_review_board =
if let Some(separate_board) = bounty_info.supervision_committee() {
separate_board
} else {
bounty_info.acceptance_committee()
};
ensure!(
Self::account_can_trigger_review(&caller, milestone_review_board),
Error::<T>::CannotPollMilestoneReviewUnlessMember
);
let milestone_submission = <MilestoneSubmissions<T>>::get(bounty_id, milestone_id)
.ok_or(Error::<T>::CannotPollMilestoneIfMilestoneSubmissionDNE)?;
match milestone_submission.state() {
MilestoneStatus::SubmittedReviewStarted(wrapped_vote_id) => {
let passed = Self::check_vote_status(wrapped_vote_id)?;
if passed {
let application = <BountyApplications<T>>::get(
bounty_id,
milestone_submission.application_id(),
)
.ok_or(Error::<T>::CannotPollMilestoneIfReferenceApplicationDNE)?;
let new_milestone_submission = if let Some(new_application) =
application.spend_approved_grant(milestone_submission.amount())
{
let transfer_id = <<T as Trait>::Bank as BankReservations<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
IpfsReference,
>>::transfer_spending_power(
caller,
bounty_info.bank_account().into(),
milestone_submission.submission(),
bounty_info.spend_reservation(),
milestone_submission.amount(),
milestone_submission.team().into(),
)?;
<BountyApplications<T>>::insert(
bounty_id,
milestone_submission.application_id(),
new_application,
);
milestone_submission
.set_make_transfer(bounty_info.bank_account(), transfer_id)
} else {
milestone_submission.approve_without_transfer()
};
let new_milestone_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(
bounty_id,
milestone_id,
new_milestone_submission,
);
Ok(new_milestone_state)
} else {
Ok(milestone_submission.state())
}
}
MilestoneStatus::ApprovedButNotTransferred => {
let application =
<BountyApplications<T>>::get(bounty_id, milestone_submission.application_id())
.ok_or(Error::<T>::CannotPollMilestoneIfReferenceApplicationDNE)?;
if let Some(new_application) =
application.spend_approved_grant(milestone_submission.amount())
{
let transfer_id = <<T as Trait>::Bank as BankReservations<
T::AccountId,
WithdrawalPermissions<T::AccountId>,
BalanceOf<T>,
IpfsReference,
>>::transfer_spending_power(
caller,
bounty_info.bank_account().into(),
milestone_submission.submission(),
bounty_info.spend_reservation(),
milestone_submission.amount(),
milestone_submission.team().into(),
)?;
let new_milestone_submission = milestone_submission
.set_make_transfer(bounty_info.bank_account(), transfer_id);
let new_milestone_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(
bounty_id,
milestone_id,
new_milestone_submission,
);
<BountyApplications<T>>::insert(
bounty_id,
milestone_submission.application_id(),
new_application,
);
Ok(new_milestone_state)
} else {
Ok(milestone_submission.state())
}
}
_ => Ok(milestone_submission.state()),
}
}
}