This is what I came up with:
Setup
pub enum VaultLabel {
None,
Treasury,
Community,
Operations,
}
#[account]
pub struct Vault {
pub bump: u8,
pub pct: u8,
pub label: VaultLabel,
}
Then I create an instruction context that initialises all the required vaults. Each vault just becomes a global PDA with a predefined seed, where the PDA can own some token accounts:
pub struct VaultCreate<'info> {
#[account(
init,
seeds = [SEED_TREASURY],
bump,
payer = payer,
space = Vault::LEN,
)]
pub treasury: Account<'info, Vault>,
#[account(
init,
seeds = [SEED_COMMUNITY],
bump,
payer = payer,
space = Vault::LEN,
)]
pub community: Account<'info, Vault>,
...
}
pub struct VaultCreateParams {
pub percentages: {treasury: u8, community: u8, operations: u8},
}
Note
payer
can be restricted to be an admin of a specific pubkey.- All the vaults are initialized AT ONCE, since this ensures you can maintain a maximum distribution percentage of 100%. So you would check that
VaultCreateParams
add up to precisely 100. - Using PDAs to maintain the vaults since this is likely more secure than regular wallet System Accounts
Distribution logic
If you have an instruction for payment, you could incorporate the distribution logic therein (of course, this depends on your particular use-case)
- Pass in the vaults in addition to their token accounts as
remaining_accounts
. Deserialize withAccount::<'info, Vault>
andAccount::<'info, TokenAccount>::try_from
. Combining with address lookup tables and versioned transactions, the extra account overhead is negligible, especially when those accounts remain constant - Check that the vault address PDA is the right pubkey
- Check that the vault owns the vault token account
- Do token transfers according to the
vault.pct
- Check that each distribution endpoint has been processed exactly once. I.e. in a for loop you would have to check if it has looped through the expected vault.labels or accounts