Sign in

Pay-as-you-act

Suggest an edit

Protocols very frequently use the pattern of distributing computations across users and having them pay only for their part. We call this Pay-As-You-Act: with this pattern, everyone pays for their own piece of the work, at the moment of their own action, instead of paying for the whole protocol at once.

Take an election as an example.

  • The bad design stores the votes. When the voting period closes, someone walks through all of the votes to find the winner. That iterating transaction pays for every vote, and nobody would volunteer to execute that transaction, as it's a common effort shifted onto a single person.
  • Instead, the good design gives each action its piece of the final step. When a vote comes in, the program immediately increases the candidate's count and compares it with the leader's count, without waiting for the final resolution window. If the count is higher, this candidate becomes the new leader. Each voter executes one addition and one comparison, triggered by that voter. When the voting period is finished, the protocol already knows the winner, as the leader is already recorded in the state, and the closing transaction reads one field.

Most of the "at the end" steps can be embedded into intermediate in-process transactions: an election winner, a payout, an average number. The key to the pattern is to store the answer instead of inputs, and let every action update it. The answer has to be something that one action can update in one step: a count, a total, a maximum, an average, a leader.

Pay-as-you-actA timeline of five actions. Each action carries a small block of work and adds it to the answer box, which grows step by step. At the end of the timeline there is no big block of work left, only a checkmark. Pay-as-you-act time action action action action action answer end: Every action does its piece of the final step at once, so nobody pays for the whole step at the end.

Examples

Hxro's parimutuel (pool betting) markets. A bet on "up" or "down" adds its amount to that side's total. When the round ends, the oracle names the winning side, and the losing pool goes to the winners. Each winner claims on their own: their share is their bet divided by the winning total. Bettors paid for the totals when they bet, and each winner pays for his own payout when he claims.

Realms (SPL Governance) lets token holders vote on proposals. Each vote updates the proposal's totals right on the spot, and the cost of that update is paid in the voting transaction. When voting ends, the program checks the totals instead of counting all the votes.

The same approach DEXes (e.g. Orca and Drift V2) utilizes in calculations of liquidity. Once maker (liquidity provider) adds or removes liquidity, they only update the metric stored in the two (or more) ticks of their range as they do not need other ticks. During the swap process taker uses only the ticks swap crosses. In this way liqudity is managed by the users which update it step-by-step.

In code

Each action updates the answer in one step, so no final tally is ever needed.

rust
// one vote bumps its candidate's count and updates the leader, nothing else
pub fn vote(ctx: Context<Vote>) -> Result<()> {
    let candidate = &mut ctx.accounts.candidate;
    let election = &mut ctx.accounts.election;
    candidate.votes += 1;
    if candidate.votes > election.leader_votes {
        election.leader = candidate.key();
        election.leader_votes = candidate.votes;
    }
    Ok(())
}