Transactions

contribute-crowdloan

Contribute TAO to an active crowdloan.

View as Markdown

Moves the amount from the signer into the crowdloan's pot. The contribution must meet the loan's minimum (and per-contributor maximum, if set) and the loan must still be open — contributing after end or past the cap fails. Contributions are not refunded automatically if the loan fails: they come back when the creator calls refund_crowdloan, or the contributor can withdraw_crowdloan themselves any time before finalization. On success, the funds go to the loan's target or fund its inner call. Pass all to contribute the whole free balance minus the existential deposit and a small fee headroom.

SignerOriginPalletWraps
coldkeysigned account (pallet role may apply)CrowdloanCrowdloan.contribute

Parameters

ParameterTypeRequiredDescription
crowdloan_idintegeryesIdentifier of the crowdloan, assigned when it was created.
amount_taonumber | "all"yesAmount to contribute, or all. Moved into the crowdloan's pot. Recoverable via refund or withdraw while the loan is not finalized.

Address parameters (--hotkey, --coldkey, --dest, ...) accept a raw ss58 address, an address-book or proxy-book name, or a local wallet/hotkey name.

CLI

Preview with --dry-run (shows fee, effects, and policy result without submitting), then submit:

btcli tx contribute-crowdloan \
  --crowdloan-id <int> \
  --amount-tao <amount|all> --dry-run
btcli tx contribute-crowdloan \
  --crowdloan-id <int> \
  --amount-tao <amount|all> -w my_coldkey

Python

import bittensor as bt
from bittensor.wallet import Wallet

wallet = Wallet(name="my_coldkey", hotkey="my_hotkey")
intent = bt.ContributeCrowdloan(crowdloan_id=0, amount_tao=1.0)

sub = bt.Subtensor()
plan = sub.plan(intent, wallet)   # fee, effects, policy — no submission
result = sub.execute(intent, wallet)
if not result.success:
    print(result.error.code, result.error.remediation)

(bt.Subtensor is also the async client — async with bt.Subtensor() as client: — see The client.) Or build the intent by op name, as an agent would:

result = sub.execute_tool("contribute_crowdloan", {...}, wallet)

On-chain implementation

Crowdloan.contributepallets/crowdloan/src/lib.rs#L440:

#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::contribute())]
pub fn contribute(
    origin: OriginFor<T>,
    #[pallet::compact] crowdloan_id: CrowdloanId,
    #[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResult {
    let contributor = ensure_signed(origin)?;
    let now = frame_system::Pallet::<T>::block_number();

    let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?;

    // Ensure crowdloan has not ended and has not raised cap
    ensure!(now < crowdloan.end, Error::<T>::ContributionPeriodEnded);
    ensure!(crowdloan.raised < crowdloan.cap, Error::<T>::CapRaised);

    // Ensure contribution is at least the minimum contribution
    ensure!(
        amount >= crowdloan.min_contribution,
        Error::<T>::ContributionTooLow
    );

    // Ensure the crowdloan has not reached the maximum number of contributors
    ensure!(
        crowdloan.contributors_count < T::MaxContributors::get(),
        Error::<T>::MaxContributorsReached
    );

    // Compute how much room is left before the crowdloan reaches its cap.
    let left_to_raise = crowdloan
        .cap
        .checked_sub(&crowdloan.raised)
        .ok_or(Error::<T>::Underflow)?;

    // The requested contribution must meet the minimum contribution, but
    // the accepted amount may be lower when only a smaller remainder can
    // be accepted before reaching the crowdloan cap or the contributor's
    // maximum contribution.
    let amount = if let Some(max_contribution) = MaxContributions::<T>::get(crowdloan_id) {
        let current_contribution =
            Contributions::<T>::get(crowdloan_id, &contributor).unwrap_or_else(Zero::zero);
        ensure!(
            current_contribution < max_contribution,
            Error::<T>::MaxContributionReached
        );
        let left_to_contribute = max_contribution
            .checked_sub(&current_contribution)
            .ok_or(Error::<T>::Underflow)?;
        amount.min(left_to_contribute).min(left_to_raise)
    } else {
        amount.min(left_to_raise)
    };

    // Ensure contribution does not overflow the actual raised amount
    crowdloan.raised = crowdloan
        .raised
        .checked_add(&amount)
        .ok_or(Error::<T>::Overflow)?;

    // Compute the new total contribution and ensure it does not overflow, we
    // also increment the contributor count if the contribution is new.
    let contribution =
        if let Some(contribution) = Contributions::<T>::get(crowdloan_id, &contributor) {
            contribution
                .checked_add(&amount)
                .ok_or(Error::<T>::Overflow)?
        } else {
            // We have a new contribution
            crowdloan.contributors_count = crowdloan
                .contributors_count
                .checked_add(1)
                .ok_or(Error::<T>::Overflow)?;
            amount
        };

    // Ensure contributor has enough balance to pay
    ensure!(
        CurrencyOf::<T>::balance(&contributor) >= amount,
        Error::<T>::InsufficientBalance
    );

    CurrencyOf::<T>::transfer(
        &contributor,
        &crowdloan.funds_account,
        amount,
        Preservation::Expendable,
    )?;

    Contributions::<T>::insert(crowdloan_id, &contributor, contribution);
    Crowdloans::<T>::insert(crowdloan_id, &crowdloan);

    Self::deposit_event(Event::<T>::Contributed {
        crowdloan_id,
        contributor,
        amount,
    });

    Ok(())
}

Every file is browsable under /code exactly as built into the runtime, or as plain text under /code/raw/<path> (index: /code/index.json).