Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 3 additions & 28 deletions rewards-delegated-vrf/anchor/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions rewards-delegated-vrf/anchor/dashboard/lib/vrfConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export const TEST_WHITELIST_USER = new PublicKey(
);

/**
* Derive the VRF Program Identity PDA
* This is used to validate that VRF callbacks come from the official VRF program
* The identity PDA is derived from the callback program (this rewards program), not the VRF program
* Seed: "identity", Program: rewards program (PROGRAM_ID)
* Derive this program's caller identity PDA (seed: "identity", program: PROGRAM_ID).
* The rewards program signs the randomness *request* with it.
*
* Not to be confused with the callback signer: the VRF oracle signs
* consumeRandomReward with the scoped identity PDA(["identity", PROGRAM_ID], VRF_PROGRAM_ID),
* which the program validates on-chain. Clients never pass that account.
*/
export function getVrfProgramIdentity(): PublicKey {
const IDENTITY_SEED = Buffer.from("identity");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
anchor-debug = []
custom-heap = []
custom-panic = []

[dependencies]
anchor-lang = { version = "1.0.2", features = ["init-if-needed"] }
anchor-spl = { version = "1.0.2", features = ["idl-build", "metadata", "associated_token"] }
ephemeral-rollups-sdk = { version = "0.16.2" , features = ["anchor"] }
ephemeral-vrf-sdk = {version = "0.3.0", features = ["anchor"]}
anchor-spl = { version = "1.0.2", features = ["metadata", "associated_token"] }
ephemeral-rollups-sdk = { version = "0.16.2", features = ["anchor", "vrf"] }
spl-associated-token-account = { version = "8.0.0", features = ["no-entrypoint"] }

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anchor_lang::prelude::*;

use crate::constants;
use crate::errors::RewardError;
use crate::instructions::shared::{schedule_transfer_action, TransferSource};
use crate::state::RewardType;
use crate::ConsumeRandomReward;
Expand Down Expand Up @@ -37,12 +38,15 @@ pub fn consume_random_reward(
let reward_list = &mut ctx.accounts.reward_list;
// Log the raw randomness proof as hex for auditability
msg!("Randomness proof: {:?}", randomness.map(|b| b));
let rnd_u32 = ephemeral_vrf_sdk::rnd::random_u32(&randomness);
let rnd_u32 = ephemeral_rollups_sdk::vrf::rnd::random_u32(&randomness);
// Width of [min, max] inclusive. Kept in u64: a full u32 range (0..=u32::MAX)
// has 2^32 values, which would wrap to 0 as u32 and panic on `% 0`.
let range = (reward_list.global_range_max as u64)
.checked_sub(reward_list.global_range_min as u64)
.unwrap()
.ok_or(RewardError::InvalidDrawRange)?
+ 1;
let result = reward_list.global_range_min + (rnd_u32 % range as u32);
// `offset < range <= 2^32`, and `min + offset <= max`, so both casts are lossless.
let result = reward_list.global_range_min + ((rnd_u32 as u64) % range) as u32;
msg!("Random result: {:?} for user: {:?}", result, user.key());

for reward in reward_list.rewards.iter() {
Expand All @@ -60,7 +64,10 @@ pub fn consume_random_reward(
for reward in reward_list.rewards.iter_mut() {
if result >= reward.draw_range_min && result <= reward.draw_range_max {
found_reward = true;
if reward.redemption_count < reward.redemption_limit {
if reward.reward_mints.is_empty() {
// Checked before counting a redemption, and avoids `% 0` on mint selection.
msg!("Reward '{}' has no mints left to distribute", reward.name);
} else if reward.redemption_count < reward.redemption_limit {
reward.redemption_count = reward.redemption_count.saturating_add(1);
msg!(
"Won reward '{}' (range {}-{})",
Expand All @@ -79,7 +86,7 @@ pub fn consume_random_reward(
let mut rnd_bytes = [0u8; 32];
rnd_bytes.copy_from_slice(&randomness);
rnd_bytes.rotate_left(4);
let rnd_mint = ephemeral_vrf_sdk::rnd::random_u32(&rnd_bytes);
let rnd_mint = ephemeral_rollups_sdk::vrf::rnd::random_u32(&rnd_bytes);
let mint_index = (rnd_mint as usize) % mint_count;
reward.reward_mints.remove(mint_index)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use anchor_lang::prelude::*;
use ephemeral_rollups_sdk::consts::{MAGIC_CONTEXT_ID, MAGIC_PROGRAM_ID};
use ephemeral_vrf_sdk::instructions::{create_request_randomness_ix, RequestRandomnessParams};
use ephemeral_vrf_sdk::types::SerializableAccountMeta;
use ephemeral_rollups_sdk::vrf::instructions::{
create_request_high_priority_scoped_randomness_ix, RequestRandomnessParams,
};
use ephemeral_rollups_sdk::vrf::types::SerializableAccountMeta;

use crate::instruction;
use crate::RequestRandomReward;
Expand Down Expand Up @@ -50,7 +52,9 @@ pub fn request_random_reward(ctx: Context<RequestRandomReward>, client_seed: u8)
&ephemeral_rollups_sdk::id(),
);

let ix = create_request_randomness_ix(RequestRandomnessParams {
// Scoped request: the oracle signs the callback with the per-program identity
// `scoped_vrf_identity(&crate::ID)`, which `ConsumeRandomReward` validates.
let ix = create_request_high_priority_scoped_randomness_ix(RequestRandomnessParams {
payer: ctx.accounts.admin.key(),
oracle_queue: ctx.accounts.oracle_queue.key(),
callback_program_id: ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::metadata::Metadata;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
use ephemeral_rollups_sdk::anchor::{action, commit, delegate, ephemeral};
use ephemeral_vrf_sdk::anchor::vrf;
use ephemeral_rollups_sdk::anchor::{vrf, vrf_callback};

pub mod constants;
pub mod errors;
Expand Down Expand Up @@ -234,18 +234,23 @@ pub struct RequestRandomReward<'info> {
#[account(seeds = [constants::TRANSFER_LOOKUP_TABLE_SEED], bump)]
pub transfer_lookup_table: Account<'info, state::TransferLookupTable>,
/// CHECK: Validated by address constraint against the known VRF oracle queue
#[account(mut, address = ephemeral_vrf_sdk::consts::DEFAULT_EPHEMERAL_QUEUE)]
#[account(mut, address = ephemeral_rollups_sdk::vrf::consts::DEFAULT_EPHEMERAL_QUEUE)]
pub oracle_queue: UncheckedAccount<'info>,
/// CHECK: reward_list delegation record; its validator derives magic_fee_vault
#[account(address = ephemeral_rollups_sdk::pda::delegation_record_pda_from_delegated_account(&reward_list.key()))]
pub delegation_record_reward_list: UncheckedAccount<'info>,
}

/// VRF callback. `#[vrf_callback]` injects `vrf_program_identity: Signer`
/// constrained to `scoped_vrf_identity(&crate::ID)`. That PDA is bound to this
/// program, so only randomness requested by this program (i.e. through the
/// gated `request_random_reward`) can reach this handler. Do NOT use the legacy
/// global `VRF_PROGRAM_IDENTITY`: it signs callbacks for any requester, which
/// would let anyone trigger reward payouts.
#[vrf_callback]
#[commit]
#[derive(Accounts)]
pub struct ConsumeRandomReward<'info> {
#[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)]
pub vrf_program_identity: Signer<'info>,
/// CHECK: reward recipient, as passed to request_random_reward
pub user: UncheckedAccount<'info>,
pub reward_distributor: Account<'info, state::RewardDistributor>,
Expand Down
Loading