joystream icon indicating copy to clipboard operation
joystream copied to clipboard

Pull-Based Periodic Budget Constrained Reward Payments

Open bedeho opened this issue 4 years ago • 1 comments

Background

A number of roles, such as council members and working group participants, all earn rewards periodically, and the spending to finance the reward comes out of a periodic budget that can be exhausted. Specifically, each participant earns a fixed number of tokens credited to a reward account at regular intervals. The problem with this is that the automatic payment, which is effectively a push-based mechanism, requires iteration over all actors, which further implies that the number must be capped at some safe upper bound. This may be satisfactory for these roles, but for example, paying out a possibly very large number of channel owners may not work. Moreover, the only remaining factor that constrains the number of workers in a working group is this payment methodology, hence having the option to drop that would also be interesting.

A natural way to work around this is to not have push payments but instead have pull-based cashouts.

Proposal

Here is a sketch, it appears to work and have no iteration over participants.


const BUDGET_PERIOD: u32;  

let recipients: Map<Id, Recipient>;

// Should be sum of `wage_per_block` of all `recipients
let totale_per_block_wage_burden;

// 
let total_owed_in_budget_period;

let budget: u32;

let nr_blocks_infeasible_payout; // since genesis, so its different

struct Recipient {
	pub nr_infeasible_payouts_before_last_cashout: u32,
	pub last_cashed_out_in_block: Option<u32>,
	pub wage_per_block: u32,
	pub destination: Account
}

pub fn on_block(block_height: u32) {

	if(block_height % BUDGET_PERIOD == 0) {

		total_owed_in_budget_period = 0;


	} else {

		total_owed_in_budget_period += totale_per_block_wage_burden;

		if (total_owed_in_budget_period > budget) {
			nr_blocks_infeasible_payout++;
		}

	}
}

pub fn add_recipient(id: Id, wage_per_block: u32) {

	recipients.insert(id, Recipient{
		nr_infeasible_payouts_before_last_cashout: 0,
		wage_per_block: wage_per_block
	})
}

pub fn cashout(recipient: & mut Recipient) {

	let nr_infeasible_payouts_after_last_cashout = nr_blocks_infeasible_payout - recipient.nr_infeasible_payouts_before_last_cashout;

	let now = current_block();

	let nr_blocks_after_last_cashout = now - last_cashed_out_in_block.ok_or(0);

	let nr_feasible_payouts_after_last_cashout = nr_blocks_after_last_cashout - nr_infeasible_payouts_after_last_cashout;

	let payable = nr_feasible_payouts_after_last_cashout*recipient.wage_per_block;

	CREDIT(recipient.destination, payable);

	// Update
	recipient.nr_infeasible_payouts_before_last_cashout += nr_infeasible_payouts_after_last_cashout;
	recipient.last_cashed_out_in_block = Some(now)
}

pub fn update_wage(recipient: & mut Recipient, new_wage_per_block: u32) {

	cashout(recipient);

	totale_per_block_wage_burden += -recipient.wage_per_block + new_wage_per_block;

	recipient.wage_per_block = new_wage_per_block;
}

pub fn update_budget(new_budget: u32) {
	budget = new_budget;
}

Questions

  1. Is this correct?
  2. Can it be further simplified?
  3. Is it too complex?

┆Issue is synchronized with this Asana task by Unito

bedeho avatar Sep 11 '20 20:09 bedeho