From 217ff74a729bdd7a15b25fac21aecea41f5760b9 Mon Sep 17 00:00:00 2001 From: Immanuelolivia1 Date: Mon, 26 Jan 2026 23:51:19 +0100 Subject: [PATCH 1/3] feat(lottery): implement token-based raffle with round lifecycle and winner selection --- Cargo.toml | 3 +- contracts/lottery/Cargo.toml | 10 ++ contracts/lottery/src/lib.rs | 191 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 contracts/lottery/Cargo.toml create mode 100644 contracts/lottery/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 58e0be2..5e7eaa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ members = [ "contracts/tournament", "contracts/bounty", "contracts/vesting", "contracts/bounty", - "contracts/insurance", + "contracts/insurance", "contracts/lottery", + "contracts/lottery", ] [workspace.dependencies] diff --git a/contracts/lottery/Cargo.toml b/contracts/lottery/Cargo.toml new file mode 100644 index 0000000..91b31c9 --- /dev/null +++ b/contracts/lottery/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "lottery" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "25.0.1" \ No newline at end of file diff --git a/contracts/lottery/src/lib.rs b/contracts/lottery/src/lib.rs new file mode 100644 index 0000000..d59a94a --- /dev/null +++ b/contracts/lottery/src/lib.rs @@ -0,0 +1,191 @@ +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, contracttype, + Env, Address, Vec, Bytes +}; + +#[contracttype] +#[derive(Clone, PartialEq)] +pub enum RoundStatus { + Open, + Drawing, + Completed, + Cancelled, +} + + +fn generate_number(env: &Env, seed: u64) -> u64 { + let mut seed_bytes = Bytes::new(&env); + seed_bytes.extend_from_slice(&seed.to_be_bytes()); + + let hash = env.crypto().sha256(&seed_bytes); + + // Convert hash to array + let hash_bytes = hash.to_array(); + + // Take first 8 bytes → u64 + u64::from_be_bytes([ + hash_bytes[0], + hash_bytes[1], + hash_bytes[2], + hash_bytes[3], + hash_bytes[4], + hash_bytes[5], + hash_bytes[6], + hash_bytes[7], + ]) +} + +// Entry point for the contract +pub fn main() { + // This function is required for the crate to compile but can remain empty for now. +} + +#[contracttype] +#[derive(Clone)] +pub struct LotteryRound { + pub id: u32, + pub ticket_price: i128, + pub prize_pool: i128, + pub start_time: u64, + pub end_time: u64, + pub winner: Option
, + pub status: RoundStatus, +} + +#[contracttype] +pub enum DataKey { + Owner, + Token, + CurrentRound, + Round(u32), + Players(u32), + Randomness(u32), +} + + +#[contract] +pub struct LotteryContract; + +#[contractimpl] +impl LotteryContract { + pub fn init(env: Env, owner: Address, token: Address) { + owner.require_auth(); + env.storage().instance().set(&DataKey::Owner, &owner); + env.storage().instance().set(&DataKey::Token, &token); + env.storage().instance().set(&DataKey::CurrentRound, &0u32); + } +} + +pub fn start_round(env: Env, ticket_price: i128, duration: u64) { + let owner: Address = env.storage().instance().get(&DataKey::Owner).unwrap(); + owner.require_auth(); + + let mut round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + round_id += 1; + + let now = env.ledger().timestamp(); + + let round = LotteryRound { + id: round_id, + ticket_price, + prize_pool: 0, + start_time: now, + end_time: now + duration, + winner: None, + status: RoundStatus::Open, + }; + + env.storage().persistent().set(&DataKey::Round(round_id), &round); + env.storage().instance().set(&DataKey::CurrentRound, &round_id); + env.storage().persistent().set(&DataKey::Players(round_id), &Vec::
::new(&env)); +} + +pub fn buy_ticket(env: Env, user: Address) { + user.require_auth(); + + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + if round.status != RoundStatus::Open { + panic!("Round not open"); + } + + let token: Address = env.storage().instance().get(&DataKey::Token).unwrap(); + let client = soroban_sdk::token::Client::new(&env, &token); + + client.transfer(&user, &env.current_contract_address(), &round.ticket_price); + + round.prize_pool += round.ticket_price; + + let mut players: Vec
= + env.storage().persistent().get(&DataKey::Players(round_id)).unwrap(); + + players.push_back(user); + + env.storage().persistent().set(&DataKey::Players(round_id), &players); + env.storage().persistent().set(&DataKey::Round(round_id), &round); +} + +fn generate_random(env: &Env) -> u64 { + let seed = env.ledger().sequence(); + let hash = env.crypto().sha256(&soroban_sdk::Bytes::from_array(env, &seed.to_be_bytes())); + let mut bytes = [0u8; 8]; + let hash_bytes = hash.to_array(); + bytes.copy_from_slice(&hash_bytes[..8]); + u64::from_be_bytes(bytes) +} + +pub fn draw_winner(env: Env) { + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + if env.ledger().timestamp() < round.end_time { + panic!("Round still active"); + } + + let players: Vec
= + env.storage().persistent().get(&DataKey::Players(round_id)).unwrap(); + + let rand = generate_random(&env); + let winner_index = (rand % players.len() as u64) as u32; + + let winner = players.get(winner_index).unwrap(); + + round.winner = Some(winner.clone()); + round.status = RoundStatus::Completed; + + env.storage().persistent().set(&DataKey::Round(round_id), &round); +} + +pub fn claim_prize(env: Env, user: Address, round_id: u32) { + user.require_auth(); + + let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + if round.winner != Some(user.clone()) { + panic!("Not winner"); + } + + let token: Address = env.storage().instance().get(&DataKey::Token).unwrap(); + let client = soroban_sdk::token::Client::new(&env, &token); + + let amount = round.prize_pool; + round.prize_pool = 0; + + client.transfer(&env.current_contract_address(), &user, &amount); + + env.storage().persistent().set(&DataKey::Round(round_id), &round); +} + +pub fn cancel_round(env: Env) { + let owner: Address = env.storage().instance().get(&DataKey::Owner).unwrap(); + owner.require_auth(); + + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + round.status = RoundStatus::Cancelled; + env.storage().persistent().set(&DataKey::Round(round_id), &round); +} \ No newline at end of file From ea7ce0129bb3ae160081eb650ead804b92b1182c Mon Sep 17 00:00:00 2001 From: Immanuelolivia1 Date: Tue, 27 Jan 2026 01:06:37 +0100 Subject: [PATCH 2/3] test(lottery): add ticket purchase and draw tests --- contracts/lottery/src/lib.rs | 18 +++++++++- contracts/lottery/src/test.rs | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 contracts/lottery/src/test.rs diff --git a/contracts/lottery/src/lib.rs b/contracts/lottery/src/lib.rs index d59a94a..306c324 100644 --- a/contracts/lottery/src/lib.rs +++ b/contracts/lottery/src/lib.rs @@ -64,6 +64,11 @@ pub enum DataKey { Randomness(u32), } +// Please note: +// Soroban does not yet support native VRF. +// This implementation uses hash-based pseudo-randomness +// suitable for MVP and educational use. + #[contract] pub struct LotteryContract; @@ -188,4 +193,15 @@ pub fn cancel_round(env: Env) { round.status = RoundStatus::Cancelled; env.storage().persistent().set(&DataKey::Round(round_id), &round); -} \ No newline at end of file +} + +pub fn refund(env: Env, round_id: u32, user: Address) { + let round = get_round(env.clone(), round_id); + assert!(round.status == RoundStatus::Cancelled); + + // transfer ticket cost back +} + +pub fn get_round (env:Env, round_id: u32) -> LotteryRound { + env.storage().persistent().get(&DataKey::CurrentRound).unwrap() +} diff --git a/contracts/lottery/src/test.rs b/contracts/lottery/src/test.rs new file mode 100644 index 0000000..6027363 --- /dev/null +++ b/contracts/lottery/src/test.rs @@ -0,0 +1,62 @@ +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{ + Env, Address, testutils::{Address as _, Ledger} + }; + + fn setup() -> (Env, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + (env, admin) + } +} + +#[test] +fn test_ticket_purchase() { + let (env, admin) = setup(); + let user = Address::generate(&env); + + Lottery::init(&env, admin.clone()); + Lottery::create_round(&env, 1, 10); + + Lottery::buy_ticket(&env, 1, user.clone()); + + let round = Lottery::get_round(&env, 1); + assert_eq!(round.tickets.len(), 1); +} + +#[test] +#[should_panic] +fn test_no_ticket_after_close() { + let (env, admin) = setup(); + let user = Address::generate(&env); + + Lottery::init(&env, admin.clone()); + Lottery::create_round(&env, 1, 10); + Lottery::close_round(&env, 1); + + Lottery::buy_ticket(&env, 1, user); +} + +#[test] +fn test_draw_winner() { + let (env, admin) = setup(); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + + Lottery::init(&env, admin.clone()); + Lottery::create_round(&env, 1, 10); + + Lottery::buy_ticket(&env, 1, user1.clone()); + Lottery::buy_ticket(&env, 1, user2.clone()); + + Lottery::close_round(&env, 1); + Lottery::draw_winner(&env, 1); + + let round = Lottery::get_round(&env, 1); + assert!(round.winner.is_some()); +} + From 28df5a5cad2cab55f69b1e9514c6f9ec99ba8ab3 Mon Sep 17 00:00:00 2001 From: Immanuelolivia1 Date: Tue, 27 Jan 2026 20:06:59 +0100 Subject: [PATCH 3/3] hot fixes --- contracts/lottery/src/lib.rs | 108 ++++++++++--------- contracts/lottery/src/test.rs | 188 +++++++++++++++++++++++++++------- 2 files changed, 204 insertions(+), 92 deletions(-) diff --git a/contracts/lottery/src/lib.rs b/contracts/lottery/src/lib.rs index 306c324..22d60fe 100644 --- a/contracts/lottery/src/lib.rs +++ b/contracts/lottery/src/lib.rs @@ -2,7 +2,7 @@ use soroban_sdk::{ contract, contractimpl, contracttype, - Env, Address, Vec, Bytes + Env, Address, Vec, Bytes, }; #[contracttype] @@ -14,34 +14,6 @@ pub enum RoundStatus { Cancelled, } - -fn generate_number(env: &Env, seed: u64) -> u64 { - let mut seed_bytes = Bytes::new(&env); - seed_bytes.extend_from_slice(&seed.to_be_bytes()); - - let hash = env.crypto().sha256(&seed_bytes); - - // Convert hash to array - let hash_bytes = hash.to_array(); - - // Take first 8 bytes → u64 - u64::from_be_bytes([ - hash_bytes[0], - hash_bytes[1], - hash_bytes[2], - hash_bytes[3], - hash_bytes[4], - hash_bytes[5], - hash_bytes[6], - hash_bytes[7], - ]) -} - -// Entry point for the contract -pub fn main() { - // This function is required for the crate to compile but can remain empty for now. -} - #[contracttype] #[derive(Clone)] pub struct LotteryRound { @@ -52,6 +24,7 @@ pub struct LotteryRound { pub end_time: u64, pub winner: Option
, pub status: RoundStatus, + pub claimed: bool, // 👈 prevents double-claim } #[contracttype] @@ -61,15 +34,8 @@ pub enum DataKey { CurrentRound, Round(u32), Players(u32), - Randomness(u32), } -// Please note: -// Soroban does not yet support native VRF. -// This implementation uses hash-based pseudo-randomness -// suitable for MVP and educational use. - - #[contract] pub struct LotteryContract; @@ -77,6 +43,7 @@ pub struct LotteryContract; impl LotteryContract { pub fn init(env: Env, owner: Address, token: Address) { owner.require_auth(); + env.storage().instance().set(&DataKey::Owner, &owner); env.storage().instance().set(&DataKey::Token, &token); env.storage().instance().set(&DataKey::CurrentRound, &0u32); @@ -100,18 +67,20 @@ pub fn start_round(env: Env, ticket_price: i128, duration: u64) { end_time: now + duration, winner: None, status: RoundStatus::Open, + claimed: false, }; env.storage().persistent().set(&DataKey::Round(round_id), &round); - env.storage().instance().set(&DataKey::CurrentRound, &round_id); env.storage().persistent().set(&DataKey::Players(round_id), &Vec::
::new(&env)); + env.storage().instance().set(&DataKey::CurrentRound, &round_id); } pub fn buy_ticket(env: Env, user: Address) { user.require_auth(); let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); - let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + let mut round: LotteryRound = + env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); if round.status != RoundStatus::Open { panic!("Round not open"); @@ -121,7 +90,6 @@ pub fn buy_ticket(env: Env, user: Address) { let client = soroban_sdk::token::Client::new(&env, &token); client.transfer(&user, &env.current_contract_address(), &round.ticket_price); - round.prize_pool += round.ticket_price; let mut players: Vec
= @@ -135,16 +103,19 @@ pub fn buy_ticket(env: Env, user: Address) { fn generate_random(env: &Env) -> u64 { let seed = env.ledger().sequence(); - let hash = env.crypto().sha256(&soroban_sdk::Bytes::from_array(env, &seed.to_be_bytes())); - let mut bytes = [0u8; 8]; - let hash_bytes = hash.to_array(); - bytes.copy_from_slice(&hash_bytes[..8]); - u64::from_be_bytes(bytes) + let hash = env.crypto().sha256(&Bytes::from_array(env, &seed.to_be_bytes())); + let bytes = hash.to_array(); + u64::from_be_bytes(bytes[..8].try_into().unwrap()) } pub fn draw_winner(env: Env) { let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); - let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + let mut round: LotteryRound = + env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + if round.status != RoundStatus::Open { + panic!("Winner already drawn"); + } if env.ledger().timestamp() < round.end_time { panic!("Round still active"); @@ -153,12 +124,14 @@ pub fn draw_winner(env: Env) { let players: Vec
= env.storage().persistent().get(&DataKey::Players(round_id)).unwrap(); - let rand = generate_random(&env); - let winner_index = (rand % players.len() as u64) as u32; + if players.len() == 0 { + panic!("No players"); + } - let winner = players.get(winner_index).unwrap(); + let rand = generate_random(&env); + let index = (rand % players.len() as u64) as u32; - round.winner = Some(winner.clone()); + round.winner = Some(players.get(index).unwrap()); round.status = RoundStatus::Completed; env.storage().persistent().set(&DataKey::Round(round_id), &round); @@ -167,7 +140,16 @@ pub fn draw_winner(env: Env) { pub fn claim_prize(env: Env, user: Address, round_id: u32) { user.require_auth(); - let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + let mut round: LotteryRound = + env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + if round.status != RoundStatus::Completed { + panic!("Round not completed"); + } + + if round.claimed { + panic!("Prize already claimed"); + } if round.winner != Some(user.clone()) { panic!("Not winner"); @@ -178,6 +160,7 @@ pub fn claim_prize(env: Env, user: Address, round_id: u32) { let amount = round.prize_pool; round.prize_pool = 0; + round.claimed = true; client.transfer(&env.current_contract_address(), &user, &amount); @@ -189,19 +172,32 @@ pub fn cancel_round(env: Env) { owner.require_auth(); let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); - let mut round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + let mut round: LotteryRound = + env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); round.status = RoundStatus::Cancelled; env.storage().persistent().set(&DataKey::Round(round_id), &round); } pub fn refund(env: Env, round_id: u32, user: Address) { + user.require_auth(); + let round = get_round(env.clone(), round_id); - assert!(round.status == RoundStatus::Cancelled); - // transfer ticket cost back -} + if round.status != RoundStatus::Cancelled { + panic!("Round not cancelled"); + } -pub fn get_round (env:Env, round_id: u32) -> LotteryRound { - env.storage().persistent().get(&DataKey::CurrentRound).unwrap() + let token: Address = env.storage().instance().get(&DataKey::Token).unwrap(); + let client = soroban_sdk::token::Client::new(&env, &token); + + client.transfer( + &env.current_contract_address(), + &user, + &round.ticket_price, + ); } + +pub fn get_round(env: Env, round_id: u32) -> LotteryRound { + env.storage().persistent().get(&DataKey::Round(round_id)).unwrap() +} \ No newline at end of file diff --git a/contracts/lottery/src/test.rs b/contracts/lottery/src/test.rs index 6027363..12afead 100644 --- a/contracts/lottery/src/test.rs +++ b/contracts/lottery/src/test.rs @@ -1,62 +1,178 @@ -#[cfg(test)] -mod test { - use super::*; - use soroban_sdk::{ - Env, Address, testutils::{Address as _, Ledger} - }; +#![cfg(test)] - fn setup() -> (Env, Address) { - let env = Env::default(); - env.mock_all_auths(); +use super::*; +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token::{Client as TokenClient}, + Address, Env, +}; - let admin = Address::generate(&env); - (env, admin) - } +fn setup_env() -> Env { + let env = Env::default(); + env.ledger().with_mut(|l| { + l.timestamp = 100; + l.sequence_number = 1; + }); + env +} + +fn setup_token(env: &Env, admin: &Address) -> (Address, TokenClient) { + let token_id = env.register_stellar_asset_contract(admin.clone()); + let token = TokenClient::new(env, &token_id); + (token_id, token) +} + +fn setup_lottery(env: &Env, owner: &Address, token: &Address) { + LotteryContract::init(env.clone(), owner.clone(), token.clone()); +} + +#[test] +fn test_init() { + let env = setup_env(); + let owner = Address::generate(&env); + let (token, _) = setup_token(&env, &owner); + + setup_lottery(&env, &owner, &token); + + let stored_owner: Address = env.storage().instance().get(&DataKey::Owner).unwrap(); + assert_eq!(stored_owner, owner); +} + +#[test] +fn test_start_round() { + let env = setup_env(); + let owner = Address::generate(&env); + let (token, _) = setup_token(&env, &owner); + + setup_lottery(&env, &owner, &token); + + start_round(env.clone(), 100, 50); + + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + assert_eq!(round.ticket_price, 100); + assert_eq!(round.status, RoundStatus::Open); } #[test] -fn test_ticket_purchase() { - let (env, admin) = setup(); +fn test_buy_ticket() { + let env = setup_env(); + let owner = Address::generate(&env); let user = Address::generate(&env); - Lottery::init(&env, admin.clone()); - Lottery::create_round(&env, 1, 10); + let (token_id, token) = setup_token(&env, &owner); + setup_lottery(&env, &owner, &token_id); + + start_round(env.clone(), 100, 50); + + token.mint(&user, &100); + token.approve( + &user, + &env.current_contract_address(), + &100, + &env.ledger().sequence(), + ); - Lottery::buy_ticket(&env, 1, user.clone()); + buy_ticket(env.clone(), user.clone()); - let round = Lottery::get_round(&env, 1); - assert_eq!(round.tickets.len(), 1); + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let players: Vec
= + env.storage().persistent().get(&DataKey::Players(round_id)).unwrap(); + + assert_eq!(players.len(), 1); } #[test] -#[should_panic] -fn test_no_ticket_after_close() { - let (env, admin) = setup(); +#[should_panic(expected = "Round not open")] +fn test_buy_ticket_closed_round() { + let env = setup_env(); + let owner = Address::generate(&env); let user = Address::generate(&env); - Lottery::init(&env, admin.clone()); - Lottery::create_round(&env, 1, 10); - Lottery::close_round(&env, 1); + let (token_id, _) = setup_token(&env, &owner); + setup_lottery(&env, &owner, &token_id); + + start_round(env.clone(), 100, 0); - Lottery::buy_ticket(&env, 1, user); + env.ledger().with_mut(|l| l.timestamp += 100); + + buy_ticket(env.clone(), user); } #[test] fn test_draw_winner() { - let (env, admin) = setup(); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); + let env = setup_env(); + let owner = Address::generate(&env); + let user = Address::generate(&env); + + let (token_id, token) = setup_token(&env, &owner); + setup_lottery(&env, &owner, &token_id); - Lottery::init(&env, admin.clone()); - Lottery::create_round(&env, 1, 10); + start_round(env.clone(), 100, 10); - Lottery::buy_ticket(&env, 1, user1.clone()); - Lottery::buy_ticket(&env, 1, user2.clone()); + token.mint(&user, &100); + token.approve( + &user, + &env.current_contract_address(), + &100, + &env.ledger().sequence(), + ); - Lottery::close_round(&env, 1); - Lottery::draw_winner(&env, 1); + buy_ticket(env.clone(), user.clone()); - let round = Lottery::get_round(&env, 1); + env.ledger().with_mut(|l| l.timestamp += 20); + + draw_winner(env.clone()); + + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + assert_eq!(round.status, RoundStatus::Completed); assert!(round.winner.is_some()); } +#[test] +#[should_panic(expected = "Not winner")] +fn test_claim_prize_not_winner() { + let env = setup_env(); + let owner = Address::generate(&env); + let user = Address::generate(&env); + + let (token_id, _) = setup_token(&env, &owner); + setup_lottery(&env, &owner, &token_id); + + start_round(env.clone(), 100, 0); + env.ledger().with_mut(|l| l.timestamp += 1); + + draw_winner(env.clone()); + + claim_prize(env.clone(), user, 1); +} + + +#[test] +fn test_cancel_round() { + let env = setup_env(); + let owner = Address::generate(&env); + let (token_id, _) = setup_token(&env, &owner); + + setup_lottery(&env, &owner, &token_id); + start_round(env.clone(), 100, 50); + + cancel_round(env.clone()); + + let round_id: u32 = env.storage().instance().get(&DataKey::CurrentRound).unwrap(); + let round: LotteryRound = env.storage().persistent().get(&DataKey::Round(round_id)).unwrap(); + + assert_eq!(round.status, RoundStatus::Cancelled); +} + +#[test] +#[should_panic] +fn test_refund_unimplemented() { + let env = setup_env(); + let user = Address::generate(&env); + + refund(env, 1, user); +} \ No newline at end of file