diff --git a/rewards-delegated-vrf/anchor/Cargo.lock b/rewards-delegated-vrf/anchor/Cargo.lock index f68c937..461ab46 100644 --- a/rewards-delegated-vrf/anchor/Cargo.lock +++ b/rewards-delegated-vrf/anchor/Cargo.lock @@ -827,8 +827,8 @@ dependencies = [ "ephemeral-rollups-sdk-attribute-delegate", "ephemeral-rollups-sdk-attribute-ephemeral", "ephemeral-rollups-sdk-attribute-ephemeral-accounts", - "ephemeral-vrf-sdk 0.4.1", - "ephemeral-vrf-sdk-vrf-macro 0.4.1", + "ephemeral-vrf-sdk", + "ephemeral-vrf-sdk-vrf-macro", "five8 0.2.1", "getrandom 0.2.16", "magicblock-delegation-program-api", @@ -895,19 +895,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ephemeral-vrf-sdk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6f6d796808e62cac2734c58731e8f7a43a7b9725587c0ceb2f94ab8b0a1421" -dependencies = [ - "anchor-lang", - "borsh 1.5.7", - "ephemeral-vrf-sdk-vrf-macro 0.3.0", - "solana-program 3.0.0", - "solana-system-interface 3.2.0", -] - [[package]] name = "ephemeral-vrf-sdk" version = "0.4.1" @@ -916,22 +903,11 @@ checksum = "77f3741c733907c10e0d0b32f9aa485c20a73e0b1e7e673a0ac38185fb2a6c11" dependencies = [ "anchor-lang", "borsh 1.5.7", - "ephemeral-vrf-sdk-vrf-macro 0.4.1", + "ephemeral-vrf-sdk-vrf-macro", "solana-program 3.0.0", "solana-system-interface 3.2.0", ] -[[package]] -name = "ephemeral-vrf-sdk-vrf-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2f2f427a08d0a9a3806034fc719f6181dc0a2d0ad990e472707a7093cf3b9d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "ephemeral-vrf-sdk-vrf-macro" version = "0.4.1" @@ -1782,7 +1758,6 @@ dependencies = [ "anchor-lang", "anchor-spl", "ephemeral-rollups-sdk", - "ephemeral-vrf-sdk 0.3.0", "spl-associated-token-account", ] diff --git a/rewards-delegated-vrf/anchor/dashboard/lib/vrfConstants.ts b/rewards-delegated-vrf/anchor/dashboard/lib/vrfConstants.ts index 6a65de4..f4284e1 100644 --- a/rewards-delegated-vrf/anchor/dashboard/lib/vrfConstants.ts +++ b/rewards-delegated-vrf/anchor/dashboard/lib/vrfConstants.ts @@ -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"); diff --git a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/Cargo.toml b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/Cargo.toml index 72a6228..092ff4a 100644 --- a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/Cargo.toml +++ b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/Cargo.toml @@ -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"] } diff --git a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/consume_random_reward.rs b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/consume_random_reward.rs index 3bd7671..7ff8835 100644 --- a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/consume_random_reward.rs +++ b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/consume_random_reward.rs @@ -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; @@ -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() { @@ -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 {}-{})", @@ -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) } diff --git a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/request_random_reward.rs b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/request_random_reward.rs index 3b4a26a..0f4dd9b 100644 --- a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/request_random_reward.rs +++ b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/instructions/request_random_reward.rs @@ -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; @@ -50,7 +52,9 @@ pub fn request_random_reward(ctx: Context, 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, diff --git a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/lib.rs b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/lib.rs index ff9ffcc..14e0f70 100644 --- a/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/lib.rs +++ b/rewards-delegated-vrf/anchor/programs/rewards-delegated-vrf/src/lib.rs @@ -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; @@ -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>,