Skip to content

feat(rewards-program): add points system#22

Open
amilz wants to merge 3 commits intomainfrom
feat/add-points-reward
Open

feat(rewards-program): add points system#22
amilz wants to merge 3 commits intomainfrom
feat/add-points-reward

Conversation

@amilz
Copy link
Collaborator

@amilz amilz commented Mar 23, 2026

Summary

  • Adds points as a fourth distribution paradigm — pure u64 PDA ledger, no token programs
  • 7 instructions: init_points, issue_points, use_points, transfer_points, close_points_account, close_points_config, revoke_points
  • 2 new accounts: PointsConfig, UserPointsAccount
  • Authority issues/revokes/closes; user cosigns use and transfer
  • Configurable: transferable flag, max_supply (0=unlimited), revocable flag

Test plan

  • 427 unit tests passing (program)
  • 406 integration tests passing (fixtures + business logic + lifecycle)
  • cargo clippy -D warnings clean on program and tests
  • cargo fmt --check clean
  • IDL regenerated and clients updated

Closes TOO-197

amilz added 3 commits March 23, 2026 08:48
Add points type as fourth distribution paradigm — pure u64 PDA ledger
with no token programs. Authority issues/closes, user cosigns use/transfer.

Accounts: PointsConfig, UserPointsAccount
Instructions: init_points, issue_points, use_points, transfer_points,
close_points_account, close_points_config
Authority can force-burn a user's points and close their account when
config.revocable is set. Gated on validate_revocable check. Revoked
balance counted as used. Also adds intentional-design comment to
close_points_config explaining admin authority over lifecycle.
7 fixture files, 8 test files covering all points instructions.
Includes generic constraint tests, business logic error paths,
PDA mismatch validation, and end-to-end lifecycle test.
@amilz amilz requested a review from dev-jodee March 23, 2026 17:01
@amilz amilz self-assigned this Mar 23, 2026
Copy link
Collaborator

@dev-jodee dev-jodee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good


#[derive(CodamaType)]
pub struct PointsAccountClosedEvent {
pub points_config: Address,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like it would be better to actually show the points config instead of just the address? Else indexers have to fetch an extra acc to get the configs


#[derive(CodamaType)]
pub struct PointsConfigClosedEvent {
pub points_config: Address,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, won't do it on all the events, but I think it should apply accross

assert_eq!(data.claimed_amount, expected_claimed_amount);
}

pub struct ExpectedPointsConfig<'a> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't really use struct to compare vs the expected, might be better to either move all of it to that, or keep following our current implementation so its coherent

user_account.balance =
user_account.balance.checked_sub(ix.data.quantity).ok_or(RewardsProgramError::MathOverflow)?;

config.total_used = config.total_used.checked_add(ix.data.quantity).ok_or(RewardsProgramError::MathOverflow)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just as an fyi, having a global config file that needs updating per instruction, means we won't be able ot use svm's parallelism as much, since write lock on the config for all point related ixs

};

// Transfer balances
from_account.balance =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to check that the 2 accounts are different, else from_account's data change will be overwritten below when you save to_account

drop(user_data);

// Require zero balance to close
if user_account.balance != 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to make it a util like

#[inline(always)]
    pub fn validate_balance(&self, amount: u64) -> Result<(), ProgramError> {
        if self.balance < amount {
            return Err(RewardsProgramError::InsufficientPointsBalance.into());
        }
        Ok(())
    }


// Count revoked points as used
if revoked_balance > 0 {
config.total_used = config.total_used.checked_add(revoked_balance).ok_or(RewardsProgramError::MathOverflow)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be added to total used ? coz they're not really "used", if they're revoked?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants