registry: Expire reward drops (#61)

This commit is contained in:
Armani Ferrante 2020-12-12 00:14:57 -08:00 committed by GitHub
parent aa1833abc5
commit 7e11fa3d7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 526 additions and 17 deletions

View File

@ -2,7 +2,9 @@ use anyhow::{anyhow, Result};
use clap::Clap;
use serum_common::client::rpc;
use serum_context::Context;
use serum_registry::accounts::{Entity, Member, Registrar};
use serum_registry::accounts::{
Entity, LockedRewardVendor, Member, Registrar, UnlockedRewardVendor,
};
use serum_registry_client::*;
use solana_client_gen::prelude::*;
@ -54,6 +56,28 @@ pub enum Command {
#[clap(short, long)]
registrar: Pubkey,
},
/// Sends all leftover funds from an expired unlocked reward vendor to a given
/// account.
ExpireUnlockedReward {
/// The token account to send the leftover rewards to.
#[clap(long)]
token: Pubkey,
#[clap(long)]
vendor: Pubkey,
#[clap(short, long)]
registrar: Pubkey,
},
/// Sends all leftover funds from an expired locked reward vendor to a given
/// account.
ExpireLockedReward {
/// The token account to send the leftover rewards to.
#[clap(long)]
token: Pubkey,
#[clap(long)]
vendor: Pubkey,
#[clap(short, long)]
registrar: Pubkey,
},
}
#[derive(Debug, Clap)]
@ -77,6 +101,14 @@ pub enum AccountsCommand {
#[clap(short, long)]
address: Option<Pubkey>,
},
LockedVendor {
#[clap(short, long)]
address: Pubkey,
},
UnlockedVendor {
#[clap(short, long)]
address: Pubkey,
},
}
pub fn run(ctx: Context, cmd: Command) -> Result<()> {
@ -121,6 +153,34 @@ pub fn run(ctx: Context, cmd: Command) -> Result<()> {
delegate,
registrar,
} => create_member_cmd(&ctx, registry_pid, registrar, entity, delegate),
Command::ExpireUnlockedReward {
token,
vendor,
registrar,
} => {
let client = ctx.connect::<Client>(registry_pid)?;
let resp = client.expire_unlocked_reward(ExpireUnlockedRewardRequest {
token,
vendor,
registrar,
})?;
println!("Transaction executed: {:?}", resp.tx);
Ok(())
}
Command::ExpireLockedReward {
token,
vendor,
registrar,
} => {
let client = ctx.connect::<Client>(registry_pid)?;
let resp = client.expire_locked_reward(ExpireLockedRewardRequest {
token,
vendor,
registrar,
})?;
println!("Transaction executed: {:?}", resp.tx);
Ok(())
}
}
}
@ -201,6 +261,14 @@ fn account_cmd(ctx: &Context, registry_pid: Pubkey, cmd: AccountsCommand) -> Res
let acc: Member = rpc::get_account(&rpc_client, &address)?;
println!("{:#?}", acc);
}
AccountsCommand::LockedVendor { address } => {
let acc: LockedRewardVendor = rpc::get_account(&rpc_client, &address)?;
println!("{:#?}", acc);
}
AccountsCommand::UnlockedVendor { address } => {
let acc: UnlockedRewardVendor = rpc::get_account(&rpc_client, &address)?;
println!("{:#?}", acc);
}
};
Ok(())
}

View File

@ -3,7 +3,8 @@ use serum_common::pack::*;
use serum_meta_entity::accounts::mqueue::{MQueue, Ring as MQueueRing};
use serum_registry::accounts::reward_queue::{RewardEventQueue, Ring};
use serum_registry::accounts::{
self, pending_withdrawal, vault, BalanceSandbox, Entity, Member, PendingWithdrawal, Registrar,
self, pending_withdrawal, vault, BalanceSandbox, Entity, LockedRewardVendor, Member,
PendingWithdrawal, Registrar, UnlockedRewardVendor,
};
use serum_registry::client::{Client as InnerClient, ClientError as InnerClientError};
use solana_client_gen::prelude::*;
@ -11,6 +12,7 @@ use solana_client_gen::solana_sdk::instruction::AccountMeta;
use solana_client_gen::solana_sdk::pubkey::Pubkey;
use solana_client_gen::solana_sdk::signature::Signature;
use solana_client_gen::solana_sdk::signature::{Keypair, Signer};
use solana_client_gen::solana_sdk::sysvar;
use spl_token::state::Account as TokenAccount;
use std::convert::Into;
use thiserror::Error;
@ -855,6 +857,64 @@ impl Client {
.switch_entity_with_signers(&[self.payer(), beneficiary], &accs)?;
Ok(SwitchEntityResponse { tx })
}
pub fn expire_unlocked_reward(
&self,
req: ExpireUnlockedRewardRequest,
) -> Result<ExpireUnlockedRewardResponse, ClientError> {
let ExpireUnlockedRewardRequest {
token,
vendor,
registrar,
} = req;
let vendor_acc = self.unlocked_vendor(&vendor)?;
let vendor_va = Pubkey::create_program_address(
&[registrar.as_ref(), vendor.as_ref(), &[vendor_acc.nonce]],
self.program(),
)
.map_err(|_| ClientError::Any(anyhow::anyhow!("invalid vendor vault authority")))?;
let accs = vec![
AccountMeta::new_readonly(self.payer().pubkey(), true),
AccountMeta::new(token, false),
AccountMeta::new(vendor, false),
AccountMeta::new(vendor_acc.vault, false),
AccountMeta::new_readonly(vendor_va, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(sysvar::clock::ID, false),
];
let tx = self.inner.expire_unlocked_reward(&accs)?;
Ok(ExpireUnlockedRewardResponse { tx })
}
pub fn expire_locked_reward(
&self,
req: ExpireLockedRewardRequest,
) -> Result<ExpireLockedRewardResponse, ClientError> {
let ExpireLockedRewardRequest {
token,
vendor,
registrar,
} = req;
let vendor_acc = self.locked_vendor(&vendor)?;
let vendor_va = Pubkey::create_program_address(
&[registrar.as_ref(), vendor.as_ref(), &[vendor_acc.nonce]],
self.program(),
)
.map_err(|_| ClientError::Any(anyhow::anyhow!("invalid vendor vault authority")))?;
let accs = vec![
AccountMeta::new_readonly(self.payer().pubkey(), true),
AccountMeta::new(token, false),
AccountMeta::new(vendor, false),
AccountMeta::new(vendor_acc.vault, false),
AccountMeta::new_readonly(vendor_va, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(sysvar::clock::ID, false),
];
let tx = self.inner.expire_locked_reward(&accs)?;
Ok(ExpireLockedRewardResponse { tx })
}
}
// Account accessors.
@ -1008,6 +1068,14 @@ impl Client {
pub fn pending_withdrawal(&self, pw: &Pubkey) -> Result<PendingWithdrawal, ClientError> {
rpc::get_account::<PendingWithdrawal>(self.rpc(), pw).map_err(Into::into)
}
pub fn unlocked_vendor(&self, v: &Pubkey) -> Result<UnlockedRewardVendor, ClientError> {
rpc::get_account::<UnlockedRewardVendor>(self.rpc(), v).map_err(Into::into)
}
pub fn locked_vendor(&self, v: &Pubkey) -> Result<LockedRewardVendor, ClientError> {
rpc::get_account::<LockedRewardVendor>(self.rpc(), v).map_err(Into::into)
}
}
pub struct ProgramAccount<T> {
@ -1247,6 +1315,26 @@ pub struct SwitchEntityResponse {
pub tx: Signature,
}
pub struct ExpireUnlockedRewardRequest {
pub token: Pubkey,
pub vendor: Pubkey,
pub registrar: Pubkey,
}
pub struct ExpireUnlockedRewardResponse {
pub tx: Signature,
}
pub struct ExpireLockedRewardRequest {
pub token: Pubkey,
pub vendor: Pubkey,
pub registrar: Pubkey,
}
pub struct ExpireLockedRewardResponse {
pub tx: Signature,
}
#[derive(Debug, Error)]
pub enum ClientError {
#[error("Client error {0}")]

View File

@ -194,6 +194,14 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
clock,
} = req;
// Move member rewards cursor.
member.rewards_cursor = cursor + 1;
if vendor.expired {
msg!("Vendor expired. Reward not collected");
return Ok(());
}
// Create vesting account with proportion of the reward.
let spt_total = spts.iter().map(|a| a.amount).fold(0, |a, b| a + b);
let amount = spt_total
@ -258,9 +266,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
)?;
}
// Move member rewards cursor.
member.rewards_cursor = cursor + 1;
Ok(())
}

View File

@ -176,6 +176,14 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
spts,
} = req;
// Move member rewards cursor.
member.rewards_cursor = cursor + 1;
if vendor.expired {
msg!("Vendor expired. Reward not collected");
return Ok(());
}
// Transfer proportion of the reward to the user.
let spt_total = spts.iter().map(|a| a.amount).fold(0, |a, b| a + b);
let amount = spt_total
@ -198,9 +206,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
amount,
)?;
// Move member rewards cursor.
member.rewards_cursor = cursor + 1;
Ok(())
}

View File

@ -0,0 +1,156 @@
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::LockedRewardVendor;
use serum_registry::error::{RegistryError, RegistryErrorCode};
use solana_program::msg;
use solana_sdk::account_info::{next_account_info, AccountInfo};
use solana_sdk::pubkey::Pubkey;
use spl_token::state::Account as TokenAccount;
#[inline(never)]
pub fn handler(program_id: &Pubkey, accounts: &[AccountInfo]) -> Result<(), RegistryError> {
msg!("handler: expire_locked_reward");
let acc_infos = &mut accounts.iter();
let expiry_receiver_acc_info = next_account_info(acc_infos)?;
let token_acc_info = next_account_info(acc_infos)?;
let vendor_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let vault_authority_acc_info = next_account_info(acc_infos)?;
let registrar_acc_info = next_account_info(acc_infos)?;
let token_program_acc_info = next_account_info(acc_infos)?;
let clock_acc_info = next_account_info(acc_infos)?;
let AccessControlResponse { ref vault } = access_control(AccessControlRequest {
program_id,
registrar_acc_info,
vendor_acc_info,
vault_acc_info,
token_acc_info,
expiry_receiver_acc_info,
clock_acc_info,
})?;
LockedRewardVendor::unpack_mut(
&mut vendor_acc_info.try_borrow_mut_data()?,
&mut |vendor: &mut LockedRewardVendor| {
state_transition(StateTransitionRequest {
vendor,
vault,
registrar_acc_info,
vendor_acc_info,
vault_acc_info,
vault_authority_acc_info,
token_acc_info,
token_program_acc_info,
})
.map_err(Into::into)
},
)
.map_err(Into::into)
}
fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, RegistryError> {
msg!("access-control: expire_locked_reward");
let AccessControlRequest {
program_id,
expiry_receiver_acc_info,
registrar_acc_info,
vault_acc_info,
vendor_acc_info,
token_acc_info,
clock_acc_info,
} = req;
// Authorization.
if !expiry_receiver_acc_info.is_signer {
return Err(RegistryErrorCode::Unauthorized)?;
}
// Account validation.
let _registrar = access_control::registrar(registrar_acc_info, program_id)?;
let vendor =
access_control::locked_reward_vendor(vendor_acc_info, registrar_acc_info, program_id)?;
let vault = access_control::token_account(vault_acc_info)?;
let token = access_control::token_account(token_acc_info)?;
let clock = access_control::clock(clock_acc_info)?;
if vendor.expired {
return Err(RegistryErrorCode::VendorAlreadyExpired)?;
}
if &vendor.vault != vault_acc_info.key {
return Err(RegistryErrorCode::InvalidVault)?;
}
if &vendor.expiry_receiver != expiry_receiver_acc_info.key {
return Err(RegistryErrorCode::InvalidVault)?;
}
if &token.owner != expiry_receiver_acc_info.key {
return Err(RegistryErrorCode::InvalidAccountOwner)?;
}
if clock.unix_timestamp <= vendor.expiry_ts {
return Err(RegistryErrorCode::VendorNotExpired)?;
}
Ok(AccessControlResponse { vault })
}
fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
msg!("state-transition: expire_locked_reward");
let StateTransitionRequest {
vendor,
vault,
token_acc_info,
vendor_acc_info,
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
token_program_acc_info,
} = req;
let signer_seeds = &[
registrar_acc_info.key.as_ref(),
vendor_acc_info.key.as_ref(),
&[vendor.nonce],
];
invoke_token_transfer(
vault_acc_info,
token_acc_info,
vault_authority_acc_info,
token_program_acc_info,
&[signer_seeds],
vault.amount,
)?;
vendor.expired = true;
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
expiry_receiver_acc_info: &'a AccountInfo<'b>,
registrar_acc_info: &'a AccountInfo<'b>,
vendor_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
token_acc_info: &'a AccountInfo<'b>,
clock_acc_info: &'a AccountInfo<'b>,
}
struct AccessControlResponse {
vault: TokenAccount,
}
struct StateTransitionRequest<'a, 'b, 'c> {
vendor: &'c mut LockedRewardVendor,
vault: &'c TokenAccount,
registrar_acc_info: &'a AccountInfo<'b>,
vendor_acc_info: &'a AccountInfo<'b>,
vault_authority_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
token_program_acc_info: &'a AccountInfo<'b>,
token_acc_info: &'a AccountInfo<'b>,
}

View File

@ -0,0 +1,156 @@
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::UnlockedRewardVendor;
use serum_registry::error::{RegistryError, RegistryErrorCode};
use solana_program::msg;
use solana_sdk::account_info::{next_account_info, AccountInfo};
use solana_sdk::pubkey::Pubkey;
use spl_token::state::Account as TokenAccount;
#[inline(never)]
pub fn handler(program_id: &Pubkey, accounts: &[AccountInfo]) -> Result<(), RegistryError> {
msg!("handler: expire_unlocked_reward");
let acc_infos = &mut accounts.iter();
let expiry_receiver_acc_info = next_account_info(acc_infos)?;
let token_acc_info = next_account_info(acc_infos)?;
let vendor_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let vault_authority_acc_info = next_account_info(acc_infos)?;
let registrar_acc_info = next_account_info(acc_infos)?;
let token_program_acc_info = next_account_info(acc_infos)?;
let clock_acc_info = next_account_info(acc_infos)?;
let AccessControlResponse { ref vault } = access_control(AccessControlRequest {
program_id,
registrar_acc_info,
vendor_acc_info,
vault_acc_info,
token_acc_info,
expiry_receiver_acc_info,
clock_acc_info,
})?;
UnlockedRewardVendor::unpack_mut(
&mut vendor_acc_info.try_borrow_mut_data()?,
&mut |vendor: &mut UnlockedRewardVendor| {
state_transition(StateTransitionRequest {
vendor,
vault,
registrar_acc_info,
vendor_acc_info,
vault_acc_info,
vault_authority_acc_info,
token_acc_info,
token_program_acc_info,
})
.map_err(Into::into)
},
)
.map_err(Into::into)
}
fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, RegistryError> {
msg!("access-control: expire_unlocked_reward");
let AccessControlRequest {
program_id,
expiry_receiver_acc_info,
registrar_acc_info,
vault_acc_info,
vendor_acc_info,
token_acc_info,
clock_acc_info,
} = req;
// Authorization.
if !expiry_receiver_acc_info.is_signer {
return Err(RegistryErrorCode::Unauthorized)?;
}
// Account validation.
let _registrar = access_control::registrar(registrar_acc_info, program_id)?;
let vendor =
access_control::unlocked_reward_vendor(vendor_acc_info, registrar_acc_info, program_id)?;
let vault = access_control::token_account(vault_acc_info)?;
let token = access_control::token_account(token_acc_info)?;
let clock = access_control::clock(clock_acc_info)?;
if vendor.expired {
return Err(RegistryErrorCode::VendorAlreadyExpired)?;
}
if &vendor.vault != vault_acc_info.key {
return Err(RegistryErrorCode::InvalidVault)?;
}
if &vendor.expiry_receiver != expiry_receiver_acc_info.key {
return Err(RegistryErrorCode::InvalidVault)?;
}
if &token.owner != expiry_receiver_acc_info.key {
return Err(RegistryErrorCode::InvalidAccountOwner)?;
}
if clock.unix_timestamp <= vendor.expiry_ts {
return Err(RegistryErrorCode::VendorNotExpired)?;
}
Ok(AccessControlResponse { vault })
}
fn state_transition(req: StateTransitionRequest) -> Result<(), RegistryError> {
msg!("state-transition: expire_unlocked_reward");
let StateTransitionRequest {
vendor,
vault,
token_acc_info,
vendor_acc_info,
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
token_program_acc_info,
} = req;
let signer_seeds = &[
registrar_acc_info.key.as_ref(),
vendor_acc_info.key.as_ref(),
&[vendor.nonce],
];
invoke_token_transfer(
vault_acc_info,
token_acc_info,
vault_authority_acc_info,
token_program_acc_info,
&[signer_seeds],
vault.amount,
)?;
vendor.expired = true;
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
expiry_receiver_acc_info: &'a AccountInfo<'b>,
registrar_acc_info: &'a AccountInfo<'b>,
vendor_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
token_acc_info: &'a AccountInfo<'b>,
clock_acc_info: &'a AccountInfo<'b>,
}
struct AccessControlResponse {
vault: TokenAccount,
}
struct StateTransitionRequest<'a, 'b, 'c> {
vendor: &'c mut UnlockedRewardVendor,
vault: &'c TokenAccount,
registrar_acc_info: &'a AccountInfo<'b>,
vendor_acc_info: &'a AccountInfo<'b>,
vault_authority_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
token_program_acc_info: &'a AccountInfo<'b>,
token_acc_info: &'a AccountInfo<'b>,
}

View File

@ -16,6 +16,8 @@ mod deposit;
mod drop_locked_reward;
mod drop_unlocked_reward;
mod end_stake_withdrawal;
mod expire_locked_reward;
mod expire_unlocked_reward;
mod initialize;
mod stake;
mod start_stake_withdrawal;
@ -129,6 +131,12 @@ fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8])
RegistryInstruction::ClaimUnlockedReward { cursor } => {
claim_unlocked_reward::handler(program_id, accounts, cursor)
}
RegistryInstruction::ExpireUnlockedReward => {
expire_unlocked_reward::handler(program_id, accounts)
}
RegistryInstruction::ExpireLockedReward => {
expire_locked_reward::handler(program_id, accounts)
}
};
result?;

View File

@ -12,10 +12,6 @@ pub struct LockedRewardVendor {
pub nonce: u8,
pub pool: Pubkey,
pub pool_token_supply: u64,
// The position of the reward event associated with this vendor.
// Used to perform access control on member accounts attempting
// to claim the reward. Reject any member who's cursor is greater
// than this cursor.
pub reward_event_q_cursor: u32,
pub start_ts: i64,
pub end_ts: i64,
@ -23,6 +19,7 @@ pub struct LockedRewardVendor {
pub expiry_receiver: Pubkey,
pub total: u64,
pub period_count: u64,
pub expired: bool,
}
impl LockedRewardVendor {

View File

@ -17,6 +17,7 @@ pub struct UnlockedRewardVendor {
pub expiry_ts: i64,
pub expiry_receiver: Pubkey,
pub total: u64,
pub expired: bool,
}
impl UnlockedRewardVendor {

View File

@ -80,6 +80,8 @@ pub enum RegistryErrorCode {
InvalidSpt = 66,
InvalidPendingWithdrawalVault = 67,
InvalidStakeVault = 68,
VendorAlreadyExpired = 69,
VendorNotExpired = 70,
Unknown = 1000,
}

View File

@ -183,6 +183,20 @@ pub mod instruction {
///
///
ClaimUnlockedReward { cursor: u32 },
/// Accounts:
///
/// 0. `[signer]` Expiry receiver.
/// 1. `[writable]` Token account to send leftover rewards to.
/// 2. `[writable]` Vendor.
/// 3. `[writable]` Vendor vault.
/// 4. `[]` Vendor vault authority.
/// 5. `[]` Registrar.
/// 6. `[]` Token program.
/// 7. `[]` Clock sysvar.
ExpireUnlockedReward,
/// Same as ExpireUnlockedReward, but with a LockedRewardVendor
/// account.
ExpireLockedReward,
}
}

View File

@ -21,8 +21,17 @@ STAKE_RATE_MEGA=1
REWARD_ACTIVATION_THRESHOLD=1
CONFIG_FILE=~/.config/serum/cli/dev.yaml
serum=$(pwd)/target/debug/serum
main() {
#
# Check the CLI is built or installed.
#
if ! command -v $serum &> /dev/null
then
echo "Serum CLI not installed"
exit
fi
#
# Build all programs.
#
@ -47,7 +56,7 @@ main() {
#
# Generate genesis state.
#
local genesis=$(cargo run -p serum-cli -- dev init-mint)
local genesis=$($serum dev init-mint)
local srm_mint=$(echo $genesis | jq .srmMint -r)
local msrm_mint=$(echo $genesis | jq .msrmMint -r)
@ -78,7 +87,7 @@ EOM
#
# Now intialize all the accounts.
#
local rInit=$(cargo run -p serum-cli -- --config $CONFIG_FILE \
local rInit=$($serum --config $CONFIG_FILE \
registry init \
--deactivation-timelock $DEACTIVATION_TIMELOCK \
--reward-activation-threshold $REWARD_ACTIVATION_THRESHOLD \
@ -91,7 +100,7 @@ EOM
local registrar_nonce=$(echo $rInit | jq .nonce -r)
local reward_q=$(echo $rInit | jq .rewardEventQueue -r)
local lInit=$(cargo run -p serum-cli -- --config $CONFIG_FILE \
local lInit=$($serum --config $CONFIG_FILE \
lockup \
initialize)
@ -101,7 +110,7 @@ EOM
# Initialize a node entity. Hack until we separate joining entities
# from creating member accounts.
#
local createEntity=$(cargo run -p serum-cli -- --config $CONFIG_FILE \
local createEntity=$($serum --config $CONFIG_FILE \
registry create-entity \
--registrar $registrar \
--about "This the default entity all new members join." \
@ -114,7 +123,7 @@ EOM
#
# Add the registry to the lockup program whitelist.
#
cargo run -p serum-cli -- --config $CONFIG_FILE \
$serum --config $CONFIG_FILE \
lockup gov \
--safe $safe \
whitelist-add \