9f100ee122
- anchor-lang 0.31.1 вместо 2.0-rc/anchor-next (git) - Address -> Pubkey (state/events/instructions) - убран #[discrim], добавлен #[derive(InitSpace)] + space=8+INIT_SPACE - тесты переписаны на solana-program-test 2.x (10 passed) - сборка SBPFv3 через cargo build-sbf --tools-version v1.57 - deploy успешен в нашу сеть (agave 4.2.2, SBPFv3)
371 lines
13 KiB
Rust
371 lines
13 KiB
Rust
use {
|
|
anchor_lang::{
|
|
solana_program::{
|
|
instruction::Instruction,
|
|
pubkey::Pubkey,
|
|
system_instruction,
|
|
},
|
|
AccountDeserialize, InstructionData, ToAccountMetas,
|
|
},
|
|
smart_updown::state::{Bet, Global},
|
|
solana_clock::Clock,
|
|
solana_keypair::Keypair,
|
|
solana_program_test::{BanksClient, ProgramTest, ProgramTestContext},
|
|
solana_signer::Signer,
|
|
solana_transaction::Transaction,
|
|
};
|
|
|
|
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;
|
|
const START_TIME: i64 = 1_700_000_000;
|
|
|
|
fn pid() -> Pubkey {
|
|
smart_updown::id()
|
|
}
|
|
|
|
fn global_pda() -> Pubkey {
|
|
Pubkey::find_program_address(&[b"global"], &pid()).0
|
|
}
|
|
|
|
fn bet_pda(id: u64) -> Pubkey {
|
|
Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
|
}
|
|
|
|
async fn setup() -> (ProgramTestContext, Keypair) {
|
|
let mut pt = ProgramTest::default();
|
|
pt.add_program("smart_updown", pid(), None);
|
|
// 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(),
|
|
&admin.pubkey(),
|
|
100_000_000_000,
|
|
)],
|
|
Some(&context.payer.pubkey()),
|
|
&[&context.payer],
|
|
context.last_blockhash,
|
|
)).await.unwrap();
|
|
(context, admin)
|
|
}
|
|
|
|
async fn set_clock(ctx: &mut ProgramTestContext, unix: i64) {
|
|
let mut clk: Clock = ctx.banks_client.get_sysvar().await.unwrap();
|
|
clk.unix_timestamp = unix;
|
|
ctx.set_sysvar(&clk);
|
|
}
|
|
|
|
async fn read_account_data(
|
|
bank: &BanksClient,
|
|
addr: &Pubkey,
|
|
) -> Vec<u8> {
|
|
bank.get_account(*addr).await.unwrap().expect("account exists").data
|
|
}
|
|
|
|
fn read_global_typed(data: &[u8]) -> Global {
|
|
Global::try_deserialize(&mut &data[..]).unwrap()
|
|
}
|
|
|
|
fn read_bet_typed(data: &[u8]) -> Bet {
|
|
Bet::try_deserialize(&mut &data[..]).unwrap()
|
|
}
|
|
|
|
fn make_initialize_ix(admin: Pubkey) -> Instruction {
|
|
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,
|
|
global: global_pda(),
|
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
|
}
|
|
.to_account_metas(None),
|
|
)
|
|
}
|
|
|
|
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> 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: Pubkey, bettor: Pubkey) -> 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),
|
|
)
|
|
}
|
|
|
|
async fn run_ix(
|
|
ctx: &mut ProgramTestContext,
|
|
signer: &Keypair,
|
|
ix: Instruction,
|
|
) -> Result<(), String> {
|
|
let tx = Transaction::new_signed_with_payer(
|
|
&[ix],
|
|
Some(&signer.pubkey()),
|
|
&[signer],
|
|
ctx.last_blockhash,
|
|
);
|
|
ctx.banks_client
|
|
.process_transaction(tx)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(|e| format!("{:?}", e))
|
|
}
|
|
|
|
async fn initialize(ctx: &mut ProgramTestContext, admin: &Keypair) {
|
|
run_ix(ctx, admin, make_initialize_ix(admin.pubkey()))
|
|
.await
|
|
.expect("initialize");
|
|
}
|
|
|
|
async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) {
|
|
let tx = Transaction::new_signed_with_payer(
|
|
&[system_instruction::transfer(
|
|
&ctx.payer.pubkey(),
|
|
to,
|
|
lamports,
|
|
)],
|
|
Some(&ctx.payer.pubkey()),
|
|
&[&ctx.payer],
|
|
ctx.last_blockhash,
|
|
);
|
|
ctx.banks_client.process_transaction(tx).await.unwrap();
|
|
}
|
|
|
|
async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) {
|
|
airdrop_to(ctx, &global_pda(), lamports).await;
|
|
}
|
|
|
|
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;
|
|
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());
|
|
run_ix(ctx, &bettor, create_ix).await.expect("create_bet");
|
|
(bettor, amount, entry_price)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn initialize_creates_global() {
|
|
let (mut ctx, admin) = setup().await;
|
|
initialize(&mut ctx, &admin).await;
|
|
let acc = read_account_data(&ctx.banks_client, &global_pda()).await;
|
|
let g = read_global_typed(&acc);
|
|
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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn create_bet_escrows_sol() {
|
|
let (mut ctx, admin) = setup().await;
|
|
initialize(&mut ctx, &admin).await;
|
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
|
|
|
let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap();
|
|
assert!(vault_balance >= 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);
|
|
assert_eq!(b.amount, amount);
|
|
assert_eq!(b.entry_price, entry_price);
|
|
assert_eq!(b.bet_time, START_TIME);
|
|
assert_eq!(b.expire_time, START_TIME + EXPIRY_SECONDS);
|
|
assert_eq!(b.status, 0);
|
|
assert_eq!(read_global_typed(&read_account_data(&ctx.banks_client, &global_pda()).await).counter, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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;
|
|
|
|
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 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");
|
|
|
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + 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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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;
|
|
|
|
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 close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
|
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
|
|
|
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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;
|
|
|
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
|
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");
|
|
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);
|
|
|
|
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");
|
|
assert_eq!(
|
|
ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(),
|
|
before + payout
|
|
);
|
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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;
|
|
|
|
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(
|
|
&mut ctx,
|
|
&admin,
|
|
make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()),
|
|
)
|
|
.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 close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
|
let res = run_ix(&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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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 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;
|
|
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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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;
|
|
|
|
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(
|
|
&mut ctx,
|
|
&admin,
|
|
make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()),
|
|
)
|
|
.await;
|
|
assert!(res.is_err(), "expected replay close to be rejected");
|
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), 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;
|
|
set_clock(&mut ctx, START_TIME).await;
|
|
|
|
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
|
let res = run_ix(&mut ctx, &bettor, ix).await;
|
|
assert!(res.is_err(), "expected below-min bet to be rejected");
|
|
} |