284 lines
9.7 KiB
Rust
284 lines
9.7 KiB
Rust
use {
|
|
anchor_lang::{
|
|
bytemuck, cpi::find_program_address, solana_program::instruction::Instruction, Id,
|
|
InstructionData, ToAccountMetas,
|
|
},
|
|
anchor_v2_testing::{Keypair, LiteSVM, Message, Signer, VersionedMessage, VersionedTransaction},
|
|
smart_updown::state::{Bet, Global},
|
|
};
|
|
|
|
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
|
|
const SYSTEM_ADDR: &str = "11111111111111111111111111111111";
|
|
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
|
|
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
|
|
const EXPIRY_SECONDS: i64 = 15;
|
|
|
|
type Addr = anchor_lang::prelude::Address;
|
|
|
|
fn pid() -> Addr {
|
|
smart_updown::id()
|
|
}
|
|
|
|
fn global_pda() -> Addr {
|
|
find_program_address(&[b"global"], &pid()).0
|
|
}
|
|
|
|
fn bet_pda(id: u64) -> Addr {
|
|
find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
|
}
|
|
|
|
fn setup() -> (LiteSVM, Keypair) {
|
|
let mut svm = LiteSVM::new();
|
|
let bytes = include_bytes!("../../../target/deploy/smart_updown.so");
|
|
svm.add_program(pid(), bytes).expect("deploy program");
|
|
let payer = Keypair::new();
|
|
svm.airdrop(&payer.pubkey(), 100_000_000_000).unwrap();
|
|
(svm, payer)
|
|
}
|
|
|
|
fn send(svm: &mut LiteSVM, signers: &[&Keypair], instruction: Instruction) -> Result<(), String> {
|
|
let blockhash = svm.latest_blockhash();
|
|
let msg = Message::new_with_blockhash(&[instruction], Some(&signers[0].pubkey()), &blockhash);
|
|
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers).unwrap();
|
|
let res = svm.send_transaction(tx);
|
|
res.map(|_| ()).map_err(|e| format!("{:?}", e))
|
|
}
|
|
|
|
fn initialize(svm: &mut LiteSVM, admin: &Keypair) {
|
|
let ix = Instruction::new_with_bytes(
|
|
pid(),
|
|
&smart_updown::instruction::Initialize {
|
|
min_amount: MIN_AMOUNT,
|
|
max_amount: 1_000_000_000_000,
|
|
multiplier_bps: MULTIPLIER_BPS,
|
|
expiry_seconds: EXPIRY_SECONDS,
|
|
}
|
|
.data(),
|
|
smart_updown::accounts::Initialize {
|
|
admin: admin.pubkey(),
|
|
global: global_pda(),
|
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
|
}
|
|
.to_account_metas(None),
|
|
);
|
|
send(svm, &[admin], ix).expect("initialize");
|
|
}
|
|
|
|
fn set_clock(svm: &mut LiteSVM, unix: i64) {
|
|
use solana_clock::Clock;
|
|
let mut clk = svm.get_sysvar::<Clock>();
|
|
clk.unix_timestamp = unix;
|
|
svm.set_sysvar(&clk);
|
|
}
|
|
|
|
fn fund_vault(svm: &mut LiteSVM, lamports: u64) {
|
|
svm.airdrop(&global_pda(), lamports).unwrap();
|
|
}
|
|
|
|
fn read_global(svm: &LiteSVM) -> Global {
|
|
let acc = svm.get_account(&global_pda()).expect("global account");
|
|
*bytemuck::from_bytes::<Global>(&acc.data[8..])
|
|
}
|
|
|
|
fn read_bet(svm: &LiteSVM, id: u64) -> Bet {
|
|
let acc = svm.get_account(&bet_pda(id)).expect("bet account");
|
|
*bytemuck::from_bytes::<Bet>(&acc.data[8..])
|
|
}
|
|
|
|
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Addr) -> Instruction {
|
|
Instruction::new_with_bytes(
|
|
pid(),
|
|
&smart_updown::instruction::CreateBet {
|
|
id,
|
|
side,
|
|
amount,
|
|
entry_price,
|
|
}
|
|
.data(),
|
|
smart_updown::accounts::CreateBet {
|
|
bettor,
|
|
global: global_pda(),
|
|
bet: bet_pda(id),
|
|
clock: CLOCK_ADDR.parse().unwrap(),
|
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
|
}
|
|
.to_account_metas(None),
|
|
)
|
|
}
|
|
|
|
fn make_close_ix(id: u64, exit_price: u64, relayer: Addr, bettor: Addr) -> Instruction {
|
|
Instruction::new_with_bytes(
|
|
pid(),
|
|
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
|
|
smart_updown::accounts::CloseBet {
|
|
relayer,
|
|
bet: bet_pda(id),
|
|
global: global_pda(),
|
|
bettor,
|
|
clock: CLOCK_ADDR.parse().unwrap(),
|
|
}
|
|
.to_account_metas(None),
|
|
)
|
|
}
|
|
|
|
fn create_up_bet(svm: &mut LiteSVM, admin: &Keypair, side: u64) -> (Keypair, u64, u64) {
|
|
let bettor = Keypair::new();
|
|
svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap();
|
|
set_clock(svm, 1_700_000_000);
|
|
let amount = MIN_AMOUNT;
|
|
let entry_price = 100_000_000u64;
|
|
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey());
|
|
send(svm, &[&bettor], create_ix).expect("create_bet");
|
|
(bettor, amount, entry_price)
|
|
}
|
|
|
|
#[test]
|
|
fn initialize_creates_global() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let g = read_global(&svm);
|
|
assert_eq!(g.admin, admin.pubkey());
|
|
assert_eq!(g.relayer, admin.pubkey());
|
|
assert_eq!(g.counter, 0);
|
|
assert_eq!(g.multiplier_bps, MULTIPLIER_BPS);
|
|
assert_eq!(g.expiry_seconds, EXPIRY_SECONDS);
|
|
assert_eq!(g.paused, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn create_bet_escrows_sol() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
|
|
let vault_balance = svm.get_balance(&global_pda()).unwrap();
|
|
assert!(vault_balance >= amount);
|
|
let b = read_bet(&svm, 0);
|
|
assert_eq!(b.bettor, bettor.pubkey());
|
|
assert_eq!(b.side, 0);
|
|
assert_eq!(b.amount, amount);
|
|
assert_eq!(b.entry_price, entry_price);
|
|
assert_eq!(b.bet_time, 1_700_000_000);
|
|
assert_eq!(b.expire_time, 1_700_000_000 + EXPIRY_SECONDS);
|
|
assert_eq!(b.status, 0);
|
|
assert_eq!(read_global(&svm).counter, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_user_wins_up() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
|
|
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
|
let exit_price = entry_price + 1_000;
|
|
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
|
fund_vault(&mut svm, 10 * amount);
|
|
|
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
|
send(&mut svm, &[&admin], close_ix).expect("close_bet");
|
|
|
|
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before + payout);
|
|
let b = read_bet(&svm, 0);
|
|
assert_eq!(b.status, 1);
|
|
assert_eq!(b.exit_price, exit_price);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_house_wins_down() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 1);
|
|
|
|
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
|
let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose
|
|
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
|
|
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
|
send(&mut svm, &[&admin], close_ix).expect("close_bet");
|
|
|
|
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before);
|
|
assert_eq!(read_bet(&svm, 0).status, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_equal_price_stays_open() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
|
|
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
|
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
|
fund_vault(&mut svm, 10 * amount);
|
|
|
|
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey());
|
|
send(&mut svm, &[&admin], equal_ix).expect("equal close");
|
|
assert_eq!(read_bet(&svm, 0).status, 0);
|
|
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before);
|
|
|
|
let exit_price = entry_price + 1_000;
|
|
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
|
send(&mut svm, &[&admin], second_ix).expect("second close");
|
|
assert_eq!(
|
|
svm.get_balance(&bettor.pubkey()).unwrap(),
|
|
before + payout
|
|
);
|
|
assert_eq!(read_bet(&svm, 0).status, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_before_expiry_rejected() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
|
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
|
let res = send(&mut svm, &[&admin], close_ix);
|
|
assert!(res.is_err(), "expected premature close to be rejected");
|
|
assert_eq!(read_bet(&svm, 0).status, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_wrong_relayer_rejected() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
let attacker = Keypair::new();
|
|
svm.airdrop(&attacker.pubkey(), 100_000_000_000).unwrap();
|
|
|
|
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
|
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey());
|
|
let res = send(&mut svm, &[&attacker], close_ix);
|
|
assert!(res.is_err(), "expected wrong relayer to be rejected");
|
|
assert_eq!(read_bet(&svm, 0).status, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn close_bet_replay_rejected() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
|
|
|
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
|
fund_vault(&mut svm, 10 * MIN_AMOUNT);
|
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
|
send(&mut svm, &[&admin], close_ix).expect("first close");
|
|
let res = send(&mut svm, &[&admin], make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()));
|
|
assert!(res.is_err(), "expected replay close to be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn create_bet_rejects_out_of_range() {
|
|
let (mut svm, admin) = setup();
|
|
initialize(&mut svm, &admin);
|
|
let bettor = Keypair::new();
|
|
svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap();
|
|
set_clock(&mut svm, 1_700_000_000);
|
|
|
|
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
|
let res = send(&mut svm, &[&bettor], ix);
|
|
assert!(res.is_err(), "expected below-min bet to be rejected");
|
|
}
|