fix: распаковать источник из обёртки smart-updown в корень ветки
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
.anchor
|
||||
.DS_Store
|
||||
target
|
||||
**/*.rs.bk
|
||||
node_modules
|
||||
test-ledger
|
||||
.yarn
|
||||
.surfpool
|
||||
@@ -0,0 +1,20 @@
|
||||
skip_local_validator = true
|
||||
|
||||
[toolchain]
|
||||
package_manager = "npm"
|
||||
|
||||
[features]
|
||||
resolution = true
|
||||
skip-lint = false
|
||||
|
||||
[programs.localnet]
|
||||
smart_updown = "E7Hg55ywRDKRZJ35rDMJGC3YFThQbh6nAK9GDayG4sr3"
|
||||
|
||||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[scripts]
|
||||
test = "cargo test"
|
||||
|
||||
[hooks]
|
||||
Generated
+4041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.89.0"
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
incremental = false
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "smart-updown"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "smart_updown"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cpi = ["no-entrypoint"]
|
||||
no-entrypoint = []
|
||||
no-log-ix-name = []
|
||||
idl-build = []
|
||||
profile = ["anchor-v2-testing/profile"]
|
||||
|
||||
[dependencies]
|
||||
# Once anchor-lang is published to crates.io, swap to: anchor-lang = "2.0.0-rc.1"
|
||||
anchor-lang = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
|
||||
solana-program-log = { version = "1.1", features = ["macro"] }
|
||||
wincode = { version = "0.5", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anchor-v2-testing = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
|
||||
solana-clock = "3.0.1"
|
||||
solana-pubkey = "3.0.0"
|
||||
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
||||
@@ -0,0 +1,16 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[constant]
|
||||
pub const GLOBAL_SEED: &[u8] = b"global";
|
||||
|
||||
#[constant]
|
||||
pub const BET_SEED: &[u8] = b"bet";
|
||||
|
||||
pub const SIDE_UP: u64 = 0;
|
||||
pub const SIDE_DOWN: u64 = 1;
|
||||
|
||||
pub const STATUS_OPEN: u64 = 0;
|
||||
pub const STATUS_PAYOUT_DONE: u64 = 1;
|
||||
pub const STATUS_HOUSE_WON: u64 = 2;
|
||||
|
||||
pub const BPS_DIVISOR: u64 = 10_000;
|
||||
@@ -0,0 +1,34 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[error_code]
|
||||
pub enum SmartUpdownError {
|
||||
#[msg("Operation is paused")]
|
||||
Paused,
|
||||
|
||||
#[msg("Only the admin can perform this action")]
|
||||
NotAdmin,
|
||||
|
||||
#[msg("Only the relayer is allowed to close bets")]
|
||||
NotRelayer,
|
||||
|
||||
#[msg("Amount below the allowed minimum")]
|
||||
AmountBelowMin,
|
||||
|
||||
#[msg("Amount above the allowed maximum")]
|
||||
AmountAboveMax,
|
||||
|
||||
#[msg("Invalid side value")]
|
||||
InvalidSide,
|
||||
|
||||
#[msg("Bet is already finalized")]
|
||||
BetAlreadyFinalized,
|
||||
|
||||
#[msg("Bet cannot be closed before expiry")]
|
||||
NotExpired,
|
||||
|
||||
#[msg("Bet id mismatch")]
|
||||
BetIdMismatch,
|
||||
|
||||
#[msg("Payout exceeds vault solvency")]
|
||||
InsufficientVault,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[event]
|
||||
pub struct BetCreated {
|
||||
pub bet: Address,
|
||||
pub bettor: Address,
|
||||
pub side: u64,
|
||||
pub amount: u64,
|
||||
pub bet_time: i64,
|
||||
pub expire_time: i64,
|
||||
pub entry_price: u64,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct BetClosed {
|
||||
pub bet: Address,
|
||||
pub exit_price: u64,
|
||||
pub user_won: bool,
|
||||
pub payout: u64,
|
||||
pub status: u64,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub mod close_bet;
|
||||
pub mod create_bet;
|
||||
pub mod initialize;
|
||||
pub mod update;
|
||||
|
||||
#[allow(ambiguous_glob_reexports)]
|
||||
pub use close_bet::*;
|
||||
pub use create_bet::*;
|
||||
pub use initialize::*;
|
||||
pub use update::*;
|
||||
@@ -0,0 +1,95 @@
|
||||
use anchor_lang::{prelude::*, Lamports};
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
BPS_DIVISOR, BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_HOUSE_WON,
|
||||
STATUS_PAYOUT_DONE, STATUS_OPEN,
|
||||
},
|
||||
error::SmartUpdownError,
|
||||
events::BetClosed,
|
||||
state::{Bet, Global},
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(id: u64, exit_price: u64)]
|
||||
pub struct CloseBet {
|
||||
pub relayer: Signer,
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [BET_SEED, id.to_le_bytes()],
|
||||
bump
|
||||
)]
|
||||
pub bet: Account<Bet>,
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [GLOBAL_SEED],
|
||||
bump
|
||||
)]
|
||||
pub global: Account<Global>,
|
||||
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
|
||||
#[account(mut)]
|
||||
pub bettor: SystemAccount,
|
||||
pub clock: Sysvar<Clock>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
|
||||
require!(
|
||||
anchor_lang::address_eq(ctx.accounts.relayer.address(), &ctx.accounts.global.relayer),
|
||||
SmartUpdownError::NotRelayer
|
||||
);
|
||||
require!(
|
||||
anchor_lang::address_eq(ctx.accounts.bettor.address(), &ctx.accounts.bet.bettor),
|
||||
SmartUpdownError::NotRelayer
|
||||
);
|
||||
require!(
|
||||
ctx.accounts.bet.status == STATUS_OPEN,
|
||||
SmartUpdownError::BetAlreadyFinalized
|
||||
);
|
||||
let now = ctx.accounts.clock.unix_timestamp;
|
||||
require!(now >= ctx.accounts.bet.expire_time, SmartUpdownError::NotExpired);
|
||||
|
||||
let entry = ctx.accounts.bet.entry_price;
|
||||
let side = ctx.accounts.bet.side;
|
||||
let amount = ctx.accounts.bet.amount;
|
||||
|
||||
if exit_price == entry {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user_won = (side == SIDE_UP && exit_price > entry)
|
||||
|| (side == SIDE_DOWN && exit_price < entry);
|
||||
|
||||
let payout;
|
||||
if user_won {
|
||||
let multiplier = ctx.accounts.global.multiplier_bps;
|
||||
let payout_amount =
|
||||
(amount as u128)
|
||||
.checked_mul(multiplier as u128)
|
||||
.unwrap()
|
||||
.checked_div(BPS_DIVISOR as u128)
|
||||
.unwrap() as u64;
|
||||
require!(
|
||||
ctx.accounts.global.view().lamports() >= payout_amount,
|
||||
SmartUpdownError::InsufficientVault
|
||||
);
|
||||
payout = payout_amount;
|
||||
ctx.accounts.global.sub_lamports(payout)?;
|
||||
ctx.accounts.bettor.add_lamports(payout)?;
|
||||
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
|
||||
} else {
|
||||
payout = 0;
|
||||
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
||||
}
|
||||
|
||||
ctx.accounts.bet.exit_price = exit_price;
|
||||
|
||||
emit!(BetClosed {
|
||||
bet: *ctx.accounts.bet.address(),
|
||||
exit_price,
|
||||
user_won,
|
||||
payout,
|
||||
status: ctx.accounts.bet.status,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN},
|
||||
error::SmartUpdownError,
|
||||
events::BetCreated,
|
||||
state::{Bet, Global},
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(id: u64, side: u64, amount: u64, entry_price: u64)]
|
||||
pub struct CreateBet {
|
||||
#[account(mut)]
|
||||
pub bettor: Signer,
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [GLOBAL_SEED],
|
||||
bump
|
||||
)]
|
||||
pub global: Account<Global>,
|
||||
#[account(
|
||||
init,
|
||||
payer = bettor,
|
||||
seeds = [BET_SEED, id.to_le_bytes()],
|
||||
bump
|
||||
)]
|
||||
pub bet: Account<Bet>,
|
||||
pub clock: Sysvar<Clock>,
|
||||
pub system_program: Program<System>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: &mut Context<CreateBet>,
|
||||
id: u64,
|
||||
side: u64,
|
||||
amount: u64,
|
||||
entry_price: u64,
|
||||
) -> Result<()> {
|
||||
require!(ctx.accounts.global.paused == 0, SmartUpdownError::Paused);
|
||||
require!(side == SIDE_UP || side == SIDE_DOWN, SmartUpdownError::InvalidSide);
|
||||
require!(
|
||||
amount >= ctx.accounts.global.min_amount,
|
||||
SmartUpdownError::AmountBelowMin
|
||||
);
|
||||
require!(
|
||||
amount <= ctx.accounts.global.max_amount,
|
||||
SmartUpdownError::AmountAboveMax
|
||||
);
|
||||
require!(
|
||||
id == ctx.accounts.global.counter,
|
||||
SmartUpdownError::BetIdMismatch
|
||||
);
|
||||
|
||||
let now = ctx.accounts.clock.unix_timestamp;
|
||||
let bettor_address = *ctx.accounts.bettor.address();
|
||||
let bet_address = *ctx.accounts.bet.address();
|
||||
let expiry_seconds = ctx.accounts.global.expiry_seconds;
|
||||
|
||||
{
|
||||
let bet = &mut ctx.accounts.bet;
|
||||
bet.bettor = bettor_address;
|
||||
bet.side = side;
|
||||
bet.amount = amount;
|
||||
bet.bet_time = now;
|
||||
bet.expire_time = now + expiry_seconds;
|
||||
bet.entry_price = entry_price;
|
||||
bet.exit_price = 0;
|
||||
bet.status = STATUS_OPEN;
|
||||
}
|
||||
|
||||
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
|
||||
|
||||
{
|
||||
let cpi_accounts = system_program::Transfer {
|
||||
from: ctx.accounts.bettor.cpi_handle_mut(),
|
||||
to: ctx.accounts.global.cpi_handle_mut(),
|
||||
};
|
||||
let cpi_ctx = CpiContext::new(ctx.accounts.system_program.address(), cpi_accounts);
|
||||
system_program::transfer(cpi_ctx, amount)?;
|
||||
}
|
||||
|
||||
emit!(BetCreated {
|
||||
bet: bet_address,
|
||||
bettor: bettor_address,
|
||||
side,
|
||||
amount,
|
||||
bet_time: now,
|
||||
expire_time: now + expiry_seconds,
|
||||
entry_price,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::{constants::GLOBAL_SEED, state::Global};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize {
|
||||
#[account(mut)]
|
||||
pub admin: Signer,
|
||||
#[account(
|
||||
init,
|
||||
payer = admin,
|
||||
seeds = [GLOBAL_SEED],
|
||||
bump
|
||||
)]
|
||||
pub global: Account<Global>,
|
||||
pub system_program: Program<System>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: &mut Context<Initialize>,
|
||||
min_amount: u64,
|
||||
max_amount: u64,
|
||||
multiplier_bps: u64,
|
||||
expiry_seconds: i64,
|
||||
) -> Result<()> {
|
||||
let global = &mut ctx.accounts.global;
|
||||
global.admin = *ctx.accounts.admin.address();
|
||||
global.relayer = *ctx.accounts.admin.address();
|
||||
global.counter = 0;
|
||||
global.min_amount = min_amount;
|
||||
global.max_amount = max_amount;
|
||||
global.multiplier_bps = multiplier_bps;
|
||||
global.paused = 0;
|
||||
global.expiry_seconds = expiry_seconds;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::GLOBAL_SEED,
|
||||
error::SmartUpdownError,
|
||||
state::Global,
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Update {
|
||||
pub admin: Signer,
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [GLOBAL_SEED],
|
||||
bump
|
||||
)]
|
||||
pub global: Account<Global>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: &mut Context<Update>,
|
||||
new_relayer: Address,
|
||||
min_amount: u64,
|
||||
max_amount: u64,
|
||||
multiplier_bps: u64,
|
||||
paused: u64,
|
||||
expiry_seconds: i64,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
anchor_lang::address_eq(ctx.accounts.admin.address(), &ctx.accounts.global.admin),
|
||||
SmartUpdownError::NotAdmin
|
||||
);
|
||||
let global = &mut ctx.accounts.global;
|
||||
global.relayer = new_relayer;
|
||||
global.min_amount = min_amount;
|
||||
global.max_amount = max_amount;
|
||||
global.multiplier_bps = multiplier_bps;
|
||||
global.paused = paused;
|
||||
global.expiry_seconds = expiry_seconds;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod instructions;
|
||||
pub mod state;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
pub use instructions::*;
|
||||
|
||||
declare_id!("E7Hg55ywRDKRZJ35rDMJGC3YFThQbh6nAK9GDayG4sr3");
|
||||
|
||||
#[program]
|
||||
pub mod smart_updown {
|
||||
use super::*;
|
||||
|
||||
#[discrim = 0]
|
||||
pub fn initialize(
|
||||
ctx: &mut Context<Initialize>,
|
||||
min_amount: u64,
|
||||
max_amount: u64,
|
||||
multiplier_bps: u64,
|
||||
expiry_seconds: i64,
|
||||
) -> Result<()> {
|
||||
initialize::handler(ctx, min_amount, max_amount, multiplier_bps, expiry_seconds)
|
||||
}
|
||||
|
||||
#[discrim = 1]
|
||||
pub fn create_bet(
|
||||
ctx: &mut Context<CreateBet>,
|
||||
id: u64,
|
||||
side: u64,
|
||||
amount: u64,
|
||||
entry_price: u64,
|
||||
) -> Result<()> {
|
||||
create_bet::handler(ctx, id, side, amount, entry_price)
|
||||
}
|
||||
|
||||
#[discrim = 2]
|
||||
pub fn close_bet(
|
||||
ctx: &mut Context<CloseBet>,
|
||||
id: u64,
|
||||
exit_price: u64,
|
||||
) -> Result<()> {
|
||||
close_bet::handler(ctx, id, exit_price)
|
||||
}
|
||||
|
||||
#[discrim = 3]
|
||||
pub fn update(
|
||||
ctx: &mut Context<Update>,
|
||||
new_relayer: Address,
|
||||
min_amount: u64,
|
||||
max_amount: u64,
|
||||
multiplier_bps: u64,
|
||||
paused: u64,
|
||||
expiry_seconds: i64,
|
||||
) -> Result<()> {
|
||||
update::handler(
|
||||
ctx,
|
||||
new_relayer,
|
||||
min_amount,
|
||||
max_amount,
|
||||
multiplier_bps,
|
||||
paused,
|
||||
expiry_seconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[account]
|
||||
pub struct Global {
|
||||
pub counter: u64,
|
||||
pub min_amount: u64,
|
||||
pub max_amount: u64,
|
||||
pub multiplier_bps: u64,
|
||||
pub expiry_seconds: i64,
|
||||
pub paused: u64,
|
||||
pub admin: Address,
|
||||
pub relayer: Address,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct Bet {
|
||||
pub amount: u64,
|
||||
pub bet_time: i64,
|
||||
pub expire_time: i64,
|
||||
pub entry_price: u64,
|
||||
pub exit_price: u64,
|
||||
pub side: u64,
|
||||
pub status: u64,
|
||||
pub bettor: Address,
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.89.0"
|
||||
components = ["rustfmt","clippy"]
|
||||
profile = "minimal"
|
||||
Reference in New Issue
Block a user