Predetermined Balances

Overview

Predetermined balances provide fine-grained control over the exact amounts and order of transfers in an approval. Unlike traditional tally-based systems where you approve a total amount (e.g., 100 tokens) without controlling the specific combinations, predetermined balances let you explicitly define:

  • Exact amounts that must be transferred

  • Specific order of transfers

  • Precise token IDs and ownership times for each transfer

Key Principle: The transfer will fail if the balances are not EXACTLY as defined in the predetermined balances.

Interface Definition

export interface PredeterminedBalances<T extends NumberType> {
    manualBalances: ManualBalances<T>[];
    incrementedBalances: IncrementedBalances<T>;
    orderCalculationMethod: PredeterminedOrderCalculationMethod;
}

Balance Definition Methods

There are two mutually exclusive ways to define balances:

1. Manual Balances

Define an array of specific balance sets manually. Each element corresponds to a different transfer.

Use Case: When you need complete control over each specific transfer amount and timing.

2. Incremented Balances

Define starting balances and rules for subsequent transfers. Perfect for sequential minting or time-based releases or other common patterns. Note that most options are incompatible with each other.

Increment Options

Field
Description
Example

incrementTokenIdsBy

Amount to increment token IDs by after each transfer

"1" = next transfer gets token ID 2, then 3, etc.

incrementOwnershipTimesBy

Amount to increment ownership times by

"86400000" = add 1 day to ownership times

durationFromTimestamp

Calculate ownership times from timestamp + duration

"2592000000" = 30 days from transfer time

allowOverrideTimestamp

Allow custom timestamp override in transfer

true = users can specify custom start time

allowOverrideWithAnyValidToken

Allow any valid token ID (one) override

true = users can specify any single valid token ID

allowAmountScaling

Allow proportional integer multiples of startBalances

true = transfer any quantity, coinTransfers scale

maxScalingMultiplier

Maximum scaling multiplier (required when scaling on)

"1000000000000" = set high for micro-unit bases

recurringOwnershipTimes

Define recurring time intervals

Monthly subscriptions, weekly rewards

Duration From Timestamp

Dynamically calculate ownership times from a timestamp plus a set duration. This overwrites all ownership times in the starting balances.

Behavior:

  • Default: Uses transfer time as the base timestamp

  • Override: If allowOverrideTimestamp is true, users can specify a custom timestamp in MsgTransferTokens precalculationOptions

  • Calculation: ownershipTime end = baseTimestamp + durationFromTimestamp - 1

  • Overwrite: All ownership times in starting balances are replaced with [{ "start": baseTimestamp, "end": baseTimestamp + durationFromTimestamp - 1 }]

Common Duration Values (milliseconds):

Duration
Milliseconds

5 minutes

300000

1 hour

3600000

1 day

86400000

1 week

604800000

30 days

2592000000

1 year

31536000000

Recurring Ownership Times

Define repeating time intervals for subscriptions or periodic rewards:

Example: Monthly subscription starting August 13, 2023, with 7-day advance charging period.

Amount Scaling

Enable proportional transfers where users can transfer any integer multiple of the base amount. When allowAmountScaling is true, startBalances defines the 1x base unit, and approvalCriteria.coinTransfers scale by the same multiplier automatically.

Constraints: When allowAmountScaling is true, all other incrementedBalances fields must be zero/false/nil. The base must be a static balance set β€” no dynamic behavior. maxScalingMultiplier must be > 0.

How it works:

  • The chain computes multiplier = transferAmount / baseAmount

  • The multiplier must be an integer >= 1 (no fractional scaling)

  • The multiplier must be <= maxScalingMultiplier

  • Each approvalCriteria.coinTransfers amount is multiplied by the same factor

  • Precalculation supports scaling via scalingMultiplier in precalculationOptions β€” set it to the desired multiplier (e.g., "5" for 5x) and the chain returns scaled balances directly. Alternatively, compute balances client-side and set them on the transfer.

Best practice: Set startBalances to the smallest possible base unit (e.g., amount: "1" for 1 micro-unit) and use a large maxScalingMultiplier. This ensures users can transfer any granular amount β€” since scaling only works with integer multiples, a micro-unit base avoids fractional limitations.

Use cases:

  • Pay-per-token: Base = 1 micro-unit, coinTransfers = 1 micro-unit of payment denom. Users buy any exact amount.

  • Prediction market deposits: Base = 1 micro-YES + 1 micro-NO. Deposit 1 USDC (1,000,000 micro) β†’ 1,000,000x multiplier.

  • Credit token purchases: Base = 1 micro-credit for 1 micro-payment. Buy any amount in one transaction.

Security considerations:

  • maxScalingMultiplier MUST be > 0 when allowAmountScaling is true β€” the chain rejects 0 (no unlimited scaling)

  • maxScalingMultiplier is enforced per transfer, not cumulatively. A user can execute multiple transfers each up to the max. To cap total exposure, pair scaling with maxNumTransfers (limit number of uses) or approvalAmounts (limit total token quantity). Without these, the only limit is the escrow/approver's available balance.

  • When coinTransfers use overrideFromWithApproverAddress: true, the escrow/approver pays multiplier * baseAmount per transfer β€” set maxScalingMultiplier conservatively and always set maxNumTransfers or approvalAmounts to bound total payout

  • Amount scaling is incompatible with Quest, Subscription, Invoice, Product, Bid/Listing, and Scheduled Payment standards (these require fixed amounts per transfer)

  • The review_collection tool flags allowAmountScaling + overrideFromWithApproverAddress as a warning for review

Precalculating Balances

The Race Condition Problem

Predetermined balances can change rapidly between transaction broadcast and confirmation. For example:

  • Other users' mints get processed

  • Token IDs shift due to concurrent activity

  • Manual balance specification becomes unreliable

The Solution: Precalculation

Use precalculateBalancesFromApproval in MsgTransferTokensarrow-up-right to dynamically calculate balances at execution time.

Precalculation Options

When using precalculateBalancesFromApproval, you can override calculation parameters. These options only apply when the corresponding flags are enabled in IncrementedBalances.

Field
Type
When It Applies
Validation

overrideTimestamp

string (Uint)

durationFromTimestamp set and allowOverrideTimestamp is true

If zero, uses current block time

tokenIdsOverride

UintRange[]

allowOverrideWithAnyValidToken is true

Must be exactly one range with start == end

scalingMultiplier

string (Uint)

allowAmountScaling is true

Must be <= maxScalingMultiplier. 0 means no scaling (returns 1x base).

overrideTimestamp: Overrides the base timestamp for ownership time calculation. Ownership times become [overrideTimestamp, overrideTimestamp + durationFromTimestamp - 1].

tokenIdsOverride: Replaces incrementally calculated token IDs. The token ID must be in the collection's validTokenIds.

scalingMultiplier: When allowAmountScaling is true, multiplies all precalculated balance amounts by this value. The chain computes the 1x base as usual, then scales. This lets API/SDK callers use standard precalculation with scaling instead of computing balances client-side.

If the corresponding flags are false, the options are ignored (no error).

Order Calculation Methods

The system needs to determine which balance set to use for each transfer. This is controlled by the orderCalculationMethod.

How Order Numbers Work

The order number determines which balances to transfer, but it works differently depending on the balance type:

Manual Balances

  • Order number = 0: Transfer manualBalances[0] (first element)

  • Order number = 1: Transfer manualBalances[1] (second element)

  • Order number = 5: Transfer manualBalances[5] (sixth element)

Example: If you have 3 manual balance sets, order numbers 0, 1, and 2 will use each set once. Order number 3 would be out of bounds.

Incremented Balances

  • Order number = 0: Use starting balances as-is (no increments)

  • Order number = 1: Apply increments once to starting balances

  • Order number = 5: Apply increments five times to starting balances

Example: Starting with token ID 1, increment by 1:

  • Order 0: Token ID 1

  • Order 1: Token ID 2

  • Order 2: Token ID 3

  • Order 5: Token ID 6

Transfer-Based Order Numbers

Track the number of transfers to determine order:

Method
Description
Use Case

useOverallNumTransfers

Global transfer count

Simple sequential transfers

usePerToAddressNumTransfers

Per-recipient count

User-specific limits

usePerFromAddressNumTransfers

Per-sender count

Sender-specific limits

usePerInitiatedByAddressNumTransfers

Per-initiator count

Initiator-specific limits

Important: Uses the same tracker as Max Number of Transfers. Trackers are:

  • Increment-only and immutable

  • Shared between predetermined balances and max transfer limits

  • Must be carefully managed to avoid conflicts

Merkle-Based Order Numbers

Use Merkle challenge leaf indices (leftmost = 0, rightmost = numLeaves - 1) for reserved transfers:

Use Case: Reserve specific token IDs for specific users or claim codes.

Order Calculation Interface

Boundary Handling

Understanding Bounds

Every approval defines bounds through its core fields (tokenIds, ownershipTimes, etc.). For example:

  • Token IDs: 1-100

  • Ownership Times: Mon-Fri only

  • Transfer Times: Specific date range

Predetermined balances must work within these bounds, but note that order numbers can eventually exceed them.

Boundary Scenarios

Complete Out-of-Bounds

Scenario: Order number corresponds to balances completely outside approval bounds.

Example:

  • Approval allows token IDs 1-100

  • Increment by 1 for each transfer

  • Order number 101 would require token ID 101 (out of bounds)

Result: Transfer is ignored because token ID 101 never matches the approval's token ID range.

Partial Overlap

Scenario: Order number corresponds to balances that partially overlap with approval bounds.

Example:

  • Approval allows token IDs 1-100

  • Transfer requires token IDs 95-105

  • Token IDs 95-100 are in bounds, 101-105 are out of bounds

Result:

  • Only in-bounds balances (95-100) are approved by current approval

  • Out-of-bounds balances (101-105) must be approved by a separate approval

  • The complete transfer (95-105) must still be exactly as defined

Important: The transfer will fail unless all out-of-bounds balances are approved by other approvals.

Last updated