feat: migrate smart-updown contract to SPL-token (фантики)

- create_bet: escrow фантиков с bettor на vault (PDA[global,vault]) через anchor_spl::token
- close_bet: выплата фантиков из vault на bettor (CPI с signer_seeds vault)
- state/constants: comments про единицы (token units decimals=6), + VAULT_SEED
- IDL регенерирован (idl/ + relayer/idl/) с аккаунтами bettor_token/vault_token/token_program
- тесты обновлены под токен (10/10 + doctest)
This commit is contained in:
Caffeine
2026-09-09 22:00:41 +03:00
parent ff2289e68d
commit 45d64d45a7
10 changed files with 1106 additions and 120 deletions
+5 -1
View File
@@ -14,10 +14,11 @@ default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-log-ix-name = []
idl-build = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
[dependencies]
anchor-lang = "0.31.1"
anchor-spl = "0.31.1"
[dev-dependencies]
anchor-lang = "0.31.1"
@@ -27,7 +28,10 @@ solana-pubkey = "2.1"
solana-signer = "2.1"
solana-keypair = "2.1"
solana-clock = "2.1"
solana-account = "2.1"
solana-transaction = "2.1"
spl-token = "7.0.0"
spl-associated-token-account = "6.0.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[lints.rust]
@@ -6,6 +6,11 @@ pub const GLOBAL_SEED: &[u8] = b"global";
#[constant]
pub const BET_SEED: &[u8] = b"bet";
/// Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).
/// It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself.
#[constant]
pub const VAULT_SEED: &[u8] = b"vault";
pub const SIDE_UP: u64 = 0;
pub const SIDE_DOWN: u64 = 1;
@@ -1,9 +1,10 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use crate::{
constants::{
BPS_DIVISOR, BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_HOUSE_WON,
STATUS_PAYOUT_DONE, STATUS_OPEN,
STATUS_PAYOUT_DONE, STATUS_OPEN, VAULT_SEED,
},
error::SmartUpdownError,
events::BetClosed,
@@ -27,9 +28,19 @@ pub struct CloseBet<'info> {
)]
pub global: Account<'info, Global>,
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
#[account(mut)]
pub bettor: SystemAccount<'info>,
pub clock: Sysvar<'info, Clock>,
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here.
#[account(
mut,
seeds = [GLOBAL_SEED, VAULT_SEED],
bump
)]
pub vault_token: Account<'info, TokenAccount>,
/// Bettor's SPL-token account; the payout is sent here.
#[account(mut)]
pub bettor_token: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
@@ -69,17 +80,36 @@ let payout;
.checked_div(BPS_DIVISOR as u128)
.unwrap() as u64;
require!(
ctx.accounts.global.get_lamports() >= payout_amount,
ctx.accounts.vault_token.amount >= payout_amount,
SmartUpdownError::InsufficientVault
);
payout = payout_amount;
ctx.accounts.global.sub_lamports(payout)?;
ctx.accounts.bettor.add_lamports(payout)?;
{
// Pay the bettor: transfer the payout from the vault to the bettor's token account.
// The vault is owned by the `global` PDA, so the CPI is signed with its seeds.
let cpi_accounts = anchor_spl::token::Transfer {
from: ctx.accounts.vault_token.to_account_info(),
to: ctx.accounts.bettor_token.to_account_info(),
authority: ctx.accounts.vault_token.to_account_info(),
};
let signer_seeds: &[&[u8]] = &[
GLOBAL_SEED,
VAULT_SEED,
&[ctx.bumps.vault_token],
];
let vault_signer_seeds = [signer_seeds];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
&vault_signer_seeds,
);
anchor_spl::token::transfer(cpi_ctx, payout)?;
}
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
} else {
payout = 0;
ctx.accounts.bet.status = STATUS_HOUSE_WON;
}
payout = 0;
ctx.accounts.bet.status = STATUS_HOUSE_WON;
}
ctx.accounts.bet.exit_price = exit_price;
@@ -1,7 +1,8 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use crate::{
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN},
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN, VAULT_SEED},
error::SmartUpdownError,
events::BetCreated,
state::{Bet, Global},
@@ -28,6 +29,17 @@ pub struct CreateBet<'info> {
pub bet: Account<'info, Bet>,
pub clock: Sysvar<'info, Clock>,
pub system_program: Program<'info, System>,
/// Bettor's SPL-token account (our "fanti" mint); bet amount is pulled from here.
#[account(mut)]
pub bettor_token: Account<'info, TokenAccount>,
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here.
#[account(
mut,
seeds = [GLOBAL_SEED, VAULT_SEED],
bump
)]
pub vault_token: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
pub fn handler(
@@ -72,15 +84,18 @@ pub fn handler(
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
{
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.bettor.to_account_info(),
to: ctx.accounts.global.to_account_info(),
// Escrow: move the bet amount (token units) from the bettor's account to the vault.
// The SPL token program enforces that `bettor_token` is owned by `bettor` (authority).
let cpi_accounts = anchor_spl::token::Transfer {
from: ctx.accounts.bettor_token.to_account_info(),
to: ctx.accounts.vault_token.to_account_info(),
authority: ctx.accounts.bettor.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
anchor_lang::system_program::transfer(cpi_ctx, amount)?;
anchor_spl::token::transfer(cpi_ctx, amount)?;
}
emit!(BetCreated {
@@ -1,5 +1,10 @@
use anchor_lang::prelude::*;
/// Global game state.
///
/// All money-related fields are denominated in **SPL-token base units**
/// (our "fanti" token, decimals = 6), not in lamports:
/// `min_amount`/`max_amount` bound a bet amount in token units.
#[account]
#[derive(InitSpace)]
pub struct Global {
@@ -13,6 +18,7 @@ pub struct Global {
pub relayer: Pubkey,
}
/// Open bet. `amount` is in token base units (decimals = 6), not lamports.
#[account]
#[derive(InitSpace)]
pub struct Bet {
@@ -2,26 +2,43 @@ use {
anchor_lang::{
solana_program::{
instruction::Instruction,
program_option::COption,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
},
AccountDeserialize, InstructionData, ToAccountMetas,
},
smart_updown::state::{Bet, Global},
smart_updown::{constants::VAULT_SEED, state::{Bet, Global}},
solana_account::{Account as SolanaAccount},
solana_clock::Clock,
solana_keypair::Keypair,
solana_program_test::{BanksClient, ProgramTest, ProgramTestContext},
solana_signer::Signer,
solana_transaction::Transaction,
spl_associated_token_account,
spl_token,
spl_token::state::{Account as TokenData, AccountState, Mint},
};
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
const SYSTEM_ADDR: &str = "11111111111111111111111111111111";
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
const MIN_AMOUNT: u64 = 1_000_000_000; // minimum bet, token units
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
const EXPIRY_SECONDS: i64 = 15;
const START_TIME: i64 = 1_700_000_000;
// SPL token (фантики) environment
const TOKEN_DECIMALS: u8 = 6;
const MINT_SIZE: u64 = 82; // classic SPL token mint account size
const TOKEN_ACCOUNT_SIZE: u64 = 165; // classic SPL token account size
const VAULT_TOKEN_SUPPLY: u64 = 100 * MIN_AMOUNT; // vault seed, covers many payouts
const BETTOR_TOKEN_SUPPLY: u64 = 10 * MIN_AMOUNT; // per-bettor token supply
/// Fixed фантики mint: mint + vault token accounts are pre-loaded into
/// genesis because the vault is a PDA and cannot sign its own create_account.
const MINT: Pubkey = Pubkey::new_from_array([3u8; 32]);
fn pid() -> Pubkey {
smart_updown::id()
}
@@ -30,16 +47,73 @@ fn global_pda() -> Pubkey {
Pubkey::find_program_address(&[b"global"], &pid()).0
}
fn vault_pda() -> Pubkey {
Pubkey::find_program_address(&[b"global", VAULT_SEED], &pid()).0
}
fn bet_pda(id: u64) -> Pubkey {
Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
}
/// Boot the program-test context with the фантики mint and the program's
/// token vault (PDA([global, vault])) pre-loaded into genesis. The vault is a
/// PDA owned by itself, so it can never sign a top-level create_account; the
/// account data below matches what the token program would have created.
async fn setup() -> (ProgramTestContext, Keypair) {
let admin = Keypair::new();
let mint_data = {
let m = Mint {
mint_authority: COption::Some(admin.pubkey()),
supply: 0,
decimals: TOKEN_DECIMALS,
is_initialized: true,
freeze_authority: COption::None,
};
let mut buf = [0u8; MINT_SIZE as usize];
Pack::pack(m, &mut buf).expect("pack mint");
buf
};
let vault_data = {
let a = TokenData {
mint: MINT,
owner: vault_pda(),
amount: VAULT_TOKEN_SUPPLY,
delegate: COption::None,
state: AccountState::Initialized,
is_native: COption::None,
delegated_amount: 0,
close_authority: COption::None,
};
let mut buf = [0u8; TOKEN_ACCOUNT_SIZE as usize];
Pack::pack(a, &mut buf).expect("pack vault token account");
buf
};
let mut pt = ProgramTest::default();
pt.add_program("smart_updown", pid(), None);
let rent = Rent::default();
pt.add_account(
MINT,
SolanaAccount {
lamports: rent.minimum_balance(MINT_SIZE as usize),
data: mint_data.to_vec(),
owner: spl_token::ID,
executable: false,
rent_epoch: 0,
},
);
pt.add_account(
vault_pda(),
SolanaAccount {
lamports: rent.minimum_balance(TOKEN_ACCOUNT_SIZE as usize),
data: vault_data.to_vec(),
owner: spl_token::ID,
executable: false,
rent_epoch: 0,
},
);
// Fund the deploy keypair separately in the test; the validator in CI funds it.
let context = pt.start_with_context().await;
let admin = Keypair::new();
context.banks_client.process_transaction(Transaction::new_signed_with_payer(
&[system_instruction::transfer(
&context.payer.pubkey(),
@@ -93,7 +167,14 @@ fn make_initialize_ix(admin: Pubkey) -> Instruction {
)
}
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> Instruction {
fn make_create_ix(
id: u64,
side: u64,
amount: u64,
entry_price: u64,
bettor: Pubkey,
bettor_ata: &Pubkey,
) -> Instruction {
Instruction::new_with_bytes(
pid(),
&smart_updown::instruction::CreateBet {
@@ -109,12 +190,21 @@ fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pub
bet: bet_pda(id),
clock: CLOCK_ADDR.parse().unwrap(),
system_program: SYSTEM_ADDR.parse().unwrap(),
bettor_token: *bettor_ata,
vault_token: vault_pda(),
token_program: spl_token::ID,
}
.to_account_metas(None),
)
}
fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> Instruction {
fn make_close_ix(
id: u64,
exit_price: u64,
relayer: Pubkey,
bettor: Pubkey,
bettor_ata: &Pubkey,
) -> Instruction {
Instruction::new_with_bytes(
pid(),
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
@@ -124,6 +214,9 @@ fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> I
global: global_pda(),
bettor,
clock: CLOCK_ADDR.parse().unwrap(),
vault_token: vault_pda(),
bettor_token: *bettor_ata,
token_program: spl_token::ID,
}
.to_account_metas(None),
)
@@ -167,22 +260,90 @@ async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) {
ctx.banks_client.process_transaction(tx).await.unwrap();
}
async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) {
airdrop_to(ctx, &global_pda(), lamports).await;
/// Create the token mint, the program's token vault (PDA([global, vault]),
/// owned by the global PDA), and seed the vault with tokens.
// The фантики env (mint + seeded vault) is pre-loaded into genesis by setup();
// a PDA vault can never sign its own top-level create_account, so this only
// returns the fixed mint the genesis accounts were built around.
async fn create_token_env(_ctx: &mut ProgramTestContext, _admin: &Keypair) -> Pubkey {
MINT
}
/// Mint tokens to a destination account (admin is the mint authority).
async fn mint_to(
ctx: &mut ProgramTestContext,
admin: &Keypair,
mint: &Pubkey,
dest: &Pubkey,
amount: u64,
) {
let ix = spl_token::instruction::mint_to(&spl_token::ID, mint, dest, &admin.pubkey(), &[], amount)
.expect("mint_to");
run_ix(ctx, admin, ix).await.expect("mint_to");
}
/// New bettor: lamports account + associated token account + token supply.
async fn create_bettor(
ctx: &mut ProgramTestContext,
admin: &Keypair,
mint: &Pubkey,
) -> (Keypair, Pubkey) {
let bettor = Keypair::new();
airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await;
let ata = spl_associated_token_account::get_associated_token_address(&bettor.pubkey(), mint);
let ix = spl_associated_token_account::instruction::create_associated_token_account(
&admin.pubkey(),
&bettor.pubkey(),
mint,
&spl_token::ID,
);
run_ix(ctx, admin, ix).await.expect("create ata");
mint_to(ctx, admin, mint, &ata, BETTOR_TOKEN_SUPPLY).await;
(bettor, ata)
}
/// Read the `amount` field of a classic SPL token account (offset 64).
async fn read_token_balance(ctx: &mut ProgramTestContext, addr: &Pubkey) -> u64 {
let data = read_account_data(&ctx.banks_client, addr).await;
u64::from_le_bytes(data[64..72].try_into().expect("amount offset"))
}
/// close_bet pays out by moving tokens out of the vault; the vault PDA signs
/// that transfer CPI inside the program, so the transaction needs only the
/// relayer's signature.
async fn run_close_tx(
ctx: &mut ProgramTestContext,
signer: &Keypair,
ix: &Instruction,
) -> Result<(), String> {
// The vault PDA signs the payout CPI inside the program (via signer_seeds);
// the transaction itself needs only the relayer's signature.
let tx = Transaction::new_signed_with_payer(
std::slice::from_ref(ix),
Some(&signer.pubkey()),
&[signer],
ctx.last_blockhash,
);
ctx.banks_client
.process_transaction(tx)
.await
.map(|_| ())
.map_err(|e| format!("{:?}", e))
}
async fn create_up_bet(
ctx: &mut ProgramTestContext,
side: u64,
) -> (Keypair, u64, u64) {
let bettor = Keypair::new();
airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await;
mint: &Pubkey,
admin: &Keypair,
) -> (Keypair, Pubkey, u64, u64) {
let (bettor, ata) = create_bettor(ctx, admin, mint).await;
set_clock(ctx, START_TIME).await;
let amount = MIN_AMOUNT;
let entry_price = 100_000_000u64;
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey());
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey(), &ata);
run_ix(ctx, &bettor, create_ix).await.expect("create_bet");
(bettor, amount, entry_price)
(bettor, ata, amount, entry_price)
}
#[tokio::test]
@@ -200,13 +361,20 @@ async fn initialize_creates_global() {
}
#[tokio::test]
async fn create_bet_escrows_sol() {
async fn create_bet_escrows_tokens() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap();
assert!(vault_balance >= amount);
assert_eq!(
read_token_balance(&mut ctx, &vault_pda()).await,
VAULT_TOKEN_SUPPLY + amount
);
assert_eq!(
read_token_balance(&mut ctx, &ata).await,
BETTOR_TOKEN_SUPPLY - amount
);
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
assert_eq!(b.bettor, bettor.pubkey());
assert_eq!(b.side, 0);
@@ -222,18 +390,27 @@ async fn create_bet_escrows_sol() {
async fn close_bet_user_wins_up() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let exit_price = entry_price + 1_000;
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
let before = read_token_balance(&mut ctx, &ata).await;
let payout = amount * MULTIPLIER_BPS / 10_000;
fund_vault(&mut ctx, 10 * amount).await;
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
run_close_tx(
&mut ctx,
&admin,
&make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata),
)
.await
.expect("close_bet");
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout);
assert_eq!(read_token_balance(&mut ctx, &ata).await, before + payout);
assert_eq!(
read_token_balance(&mut ctx, &vault_pda()).await,
VAULT_TOKEN_SUPPLY + amount - payout
);
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
assert_eq!(b.status, 1);
assert_eq!(b.exit_price, exit_price);
@@ -243,16 +420,22 @@ async fn close_bet_user_wins_up() {
async fn close_bet_house_wins_down() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 1).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 1, &mint, &admin).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
let before = read_token_balance(&mut ctx, &ata).await;
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
run_close_tx(
&mut ctx,
&admin,
&make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata),
)
.await
.expect("close_bet");
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
assert_eq!(read_token_balance(&mut ctx, &ata).await, before);
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2);
}
@@ -260,23 +443,23 @@ async fn close_bet_house_wins_down() {
async fn close_bet_equal_price_stays_open() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
let before = read_token_balance(&mut ctx, &ata).await;
let payout = amount * MULTIPLIER_BPS / 10_000;
fund_vault(&mut ctx, 10 * amount).await;
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey());
run_ix(&mut ctx, &admin, equal_ix).await.expect("equal close");
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey(), &ata);
run_close_tx(&mut ctx, &admin, &equal_ix).await.expect("equal close");
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
assert_eq!(read_token_balance(&mut ctx, &ata).await, before);
let exit_price = entry_price + 1_000;
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
run_ix(&mut ctx, &admin, second_ix).await.expect("second close");
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata);
run_close_tx(&mut ctx, &admin, &second_ix).await.expect("second close");
assert_eq!(
ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(),
read_token_balance(&mut ctx, &ata).await,
before + payout
);
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
@@ -286,37 +469,40 @@ async fn close_bet_equal_price_stays_open() {
async fn close_bet_already_finalized() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let win_price = entry_price + 500;
fund_vault(&mut ctx, 10 * amount).await;
run_ix(&mut ctx, &admin, make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey()))
.await
.expect("first close");
let res = run_ix(
run_close_tx(
&mut ctx,
&admin,
make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()),
&make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey(), &ata),
)
.await
.expect("first close");
let res = run_close_tx(
&mut ctx,
&admin,
&make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey(), &ata),
)
.await;
assert!(res.is_err(), "expected close after finalization to be rejected");
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
assert_eq!(b.status, 1);
assert_eq!(b.exit_price, win_price);
assert_eq!(b.status, 1);
assert_eq!(b.exit_price, win_price);
}
#[tokio::test]
async fn close_bet_before_expiry_rejected() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
let res = run_ix(&mut ctx, &admin, close_ix).await;
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey(), &ata);
let res = run_close_tx(&mut ctx, &admin, &close_ix).await;
assert!(res.is_err(), "expected premature close to be rejected");
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
}
@@ -325,13 +511,14 @@ async fn close_bet_before_expiry_rejected() {
async fn close_bet_wrong_relayer_rejected() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
let attacker = Keypair::new();
airdrop_to(&mut ctx, &attacker.pubkey(), 100_000_000_000).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey());
let res = run_ix(&mut ctx, &attacker, close_ix).await;
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey(), &ata);
let res = run_close_tx(&mut ctx, &attacker, &close_ix).await;
assert!(res.is_err(), "expected wrong relayer to be rejected");
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
}
@@ -340,32 +527,34 @@ async fn close_bet_wrong_relayer_rejected() {
async fn close_bet_replay_rejected() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
fund_vault(&mut ctx, 10 * MIN_AMOUNT).await;
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
run_ix(&mut ctx, &admin, close_ix).await.expect("first close");
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
let res = run_ix(
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey(), &ata);
run_close_tx(&mut ctx, &admin, &close_ix).await.expect("first close");
let before = read_token_balance(&mut ctx, &ata).await;
let res = run_close_tx(
&mut ctx,
&admin,
make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()),
&make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey(), &ata),
)
.await;
assert!(res.is_err(), "expected replay close to be rejected");
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
assert_eq!(read_token_balance(&mut ctx, &ata).await, before);
}
#[tokio::test]
async fn create_bet_rejects_out_of_range() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let bettor = Keypair::new();
airdrop_to(&mut ctx, &bettor.pubkey(), 100_000_000_000).await;
let mint = create_token_env(&mut ctx, &admin).await;
let (bettor, ata) = create_bettor(&mut ctx, &admin, &mint).await;
set_clock(&mut ctx, START_TIME).await;
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
let vault_before = read_token_balance(&mut ctx, &vault_pda()).await;
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey(), &ata);
let res = run_ix(&mut ctx, &bettor, ix).await;
assert!(res.is_err(), "expected below-min bet to be rejected");
assert_eq!(read_token_balance(&mut ctx, &vault_pda()).await, vault_before);
}