solaudit 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -12
- package/dist/cli.js +485 -41
- package/examples/solscape/ANSWER_KEY.md +35 -0
- package/examples/solscape/DESIGN.md +54 -0
- package/examples/solscape/README.md +20 -0
- package/examples/solscape/SCORECARD.md +102 -0
- package/examples/solscape/fixture_v1/lib_v1.rs +403 -0
- package/examples/solscape/programs/solscape/src/lib.rs +410 -0
- package/package.json +1 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
//! SolScape — persistent on-chain territory game.
|
|
2
|
+
//!
|
|
3
|
+
//! ⚠️ INTENTIONALLY VULNERABLE TEST FIXTURE (v2) ⚠️
|
|
4
|
+
//! This build contains deliberately seeded vulnerabilities for benchmarking the
|
|
5
|
+
//! SolAudit scanner (see ANSWER_KEY.md). DO NOT DEPLOY. The hardened build lives
|
|
6
|
+
//! in `programs/solscape-fixed/` and is the only version that may ever be deployed.
|
|
7
|
+
//!
|
|
8
|
+
//! v2 realism fixes (disclosed in SCORECARD.md):
|
|
9
|
+
//! - withdraw_treasury now uses invoke_signed (the genuinely exploitable shape)
|
|
10
|
+
//! - close_tile leaves account data intact (genuine close-then-revive)
|
|
11
|
+
//! - buy_tile_spl takes typed token accounts + a mint, with the CPI program id
|
|
12
|
+
//! sourced from an unchecked account (textbook arbitrary-CPI shape)
|
|
13
|
+
|
|
14
|
+
use anchor_lang::prelude::*;
|
|
15
|
+
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
|
|
16
|
+
use anchor_lang::solana_program::program::{invoke, invoke_signed};
|
|
17
|
+
use anchor_lang::solana_program::system_instruction;
|
|
18
|
+
use anchor_spl::token::{Mint, TokenAccount};
|
|
19
|
+
|
|
20
|
+
declare_id!("So1Scape11111111111111111111111111111111111");
|
|
21
|
+
|
|
22
|
+
pub const GRID_SIZE: u8 = 100;
|
|
23
|
+
pub const CLAIM_FEE_LAMPORTS: u64 = 10_000_000; // 0.01 SOL
|
|
24
|
+
pub const UPGRADE_BASE_FEE: u64 = 5_000_000; // 0.005 SOL
|
|
25
|
+
pub const ROYALTY_BPS: u64 = 500; // 5%
|
|
26
|
+
|
|
27
|
+
#[program]
|
|
28
|
+
pub mod solscape {
|
|
29
|
+
use super::*;
|
|
30
|
+
|
|
31
|
+
/// C1 — clean control: proper init, seeds, signer.
|
|
32
|
+
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
|
33
|
+
let config = &mut ctx.accounts.config;
|
|
34
|
+
config.authority = ctx.accounts.authority.key();
|
|
35
|
+
config.claim_fee = CLAIM_FEE_LAMPORTS;
|
|
36
|
+
config.royalty_bps = ROYALTY_BPS;
|
|
37
|
+
Ok(())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// B1 — SEEDED: init_if_needed lets anyone wipe an existing profile back to zero.
|
|
41
|
+
pub fn register_player(ctx: Context<RegisterPlayer>) -> Result<()> {
|
|
42
|
+
let profile = &mut ctx.accounts.profile;
|
|
43
|
+
profile.owner = ctx.accounts.player.key();
|
|
44
|
+
profile.xp = 0;
|
|
45
|
+
profile.tiles_claimed = 0;
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// C2 — clean control: PDA via seeds, Signer, explicit system-program CPI.
|
|
50
|
+
pub fn claim_tile(ctx: Context<ClaimTile>, x: u8, y: u8) -> Result<()> {
|
|
51
|
+
require!(x < GRID_SIZE && y < GRID_SIZE, GameError::OutOfBounds);
|
|
52
|
+
|
|
53
|
+
let tile = &mut ctx.accounts.tile;
|
|
54
|
+
tile.owner = ctx.accounts.player.key();
|
|
55
|
+
tile.x = x;
|
|
56
|
+
tile.y = y;
|
|
57
|
+
tile.level = 1;
|
|
58
|
+
tile.list_price = 0;
|
|
59
|
+
|
|
60
|
+
let fee = ctx.accounts.config.claim_fee;
|
|
61
|
+
let ix = system_instruction::transfer(
|
|
62
|
+
&ctx.accounts.player.key(),
|
|
63
|
+
&ctx.accounts.treasury.key(),
|
|
64
|
+
fee,
|
|
65
|
+
);
|
|
66
|
+
invoke(
|
|
67
|
+
&ix,
|
|
68
|
+
&[
|
|
69
|
+
ctx.accounts.player.to_account_info(),
|
|
70
|
+
ctx.accounts.treasury.to_account_info(),
|
|
71
|
+
ctx.accounts.system_program.to_account_info(),
|
|
72
|
+
],
|
|
73
|
+
)?;
|
|
74
|
+
|
|
75
|
+
let profile = &mut ctx.accounts.profile;
|
|
76
|
+
profile.tiles_claimed = profile.tiles_claimed.checked_add(1).unwrap();
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// B2 — SEEDED: price curve overflows; at high level the fee wraps to ~zero.
|
|
81
|
+
pub fn upgrade_tile(ctx: Context<UpgradeTile>) -> Result<()> {
|
|
82
|
+
let tile = &mut ctx.accounts.tile;
|
|
83
|
+
let profile = &ctx.accounts.profile;
|
|
84
|
+
|
|
85
|
+
// fee = base * 2^level * tiles_owned — unchecked pow/mul chain
|
|
86
|
+
let fee = UPGRADE_BASE_FEE
|
|
87
|
+
* 2u64.pow(tile.level as u32)
|
|
88
|
+
* profile.tiles_claimed as u64;
|
|
89
|
+
|
|
90
|
+
let ix = system_instruction::transfer(
|
|
91
|
+
&ctx.accounts.player.key(),
|
|
92
|
+
&ctx.accounts.treasury.key(),
|
|
93
|
+
fee,
|
|
94
|
+
);
|
|
95
|
+
invoke(
|
|
96
|
+
&ix,
|
|
97
|
+
&[
|
|
98
|
+
ctx.accounts.player.to_account_info(),
|
|
99
|
+
ctx.accounts.treasury.to_account_info(),
|
|
100
|
+
],
|
|
101
|
+
)?;
|
|
102
|
+
|
|
103
|
+
tile.level = tile.level + 1;
|
|
104
|
+
Ok(())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// C3 — clean control: has_one, Signer, no CPI.
|
|
108
|
+
pub fn list_tile(ctx: Context<ListTile>, price: u64) -> Result<()> {
|
|
109
|
+
let tile = &mut ctx.accounts.tile;
|
|
110
|
+
tile.list_price = price;
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// B3 — SEEDED: tile is a raw account; price/seller are trusted from
|
|
115
|
+
/// attacker-controllable bytes. Pass any crafted account as "tile".
|
|
116
|
+
pub fn buy_tile(ctx: Context<BuyTile>) -> Result<()> {
|
|
117
|
+
let tile_info = &ctx.accounts.tile;
|
|
118
|
+
let data = tile_info.try_borrow_data()?;
|
|
119
|
+
// layout: [disc(8) | owner(32) | x(1) | y(1) | level(1) | list_price(8)]
|
|
120
|
+
let list_price = u64::from_le_bytes(data[43..51].try_into().unwrap());
|
|
121
|
+
require!(list_price > 0, GameError::NotForSale);
|
|
122
|
+
|
|
123
|
+
let royalty = list_price * ROYALTY_BPS / 10_000;
|
|
124
|
+
let seller_proceeds = list_price - royalty;
|
|
125
|
+
|
|
126
|
+
**ctx.accounts.buyer.try_borrow_mut_lamports()? -= list_price;
|
|
127
|
+
**ctx.accounts.seller.try_borrow_mut_lamports()? += seller_proceeds;
|
|
128
|
+
**ctx.accounts.treasury.try_borrow_mut_lamports()? += royalty;
|
|
129
|
+
|
|
130
|
+
drop(data);
|
|
131
|
+
let mut data = tile_info.try_borrow_mut_data()?;
|
|
132
|
+
data[8..40].copy_from_slice(&ctx.accounts.buyer.key().to_bytes());
|
|
133
|
+
Ok(())
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// B4 + B5 — SEEDED: the CPI program id comes from an UNCHECKED account
|
|
137
|
+
/// (arbitrary CPI), and buyer_token is never bound to the expected mint
|
|
138
|
+
/// (pay with worthless tokens — the Cashio class).
|
|
139
|
+
pub fn buy_tile_spl(ctx: Context<BuyTileSpl>, amount: u64) -> Result<()> {
|
|
140
|
+
let ix = spl_token_transfer(
|
|
141
|
+
&ctx.accounts.token_program.key(),
|
|
142
|
+
&ctx.accounts.buyer_token.key(),
|
|
143
|
+
&ctx.accounts.seller_token.key(),
|
|
144
|
+
&ctx.accounts.buyer.key(),
|
|
145
|
+
amount,
|
|
146
|
+
);
|
|
147
|
+
invoke(
|
|
148
|
+
&ix,
|
|
149
|
+
&[
|
|
150
|
+
ctx.accounts.buyer_token.to_account_info(),
|
|
151
|
+
ctx.accounts.seller_token.to_account_info(),
|
|
152
|
+
ctx.accounts.buyer.to_account_info(),
|
|
153
|
+
ctx.accounts.token_program.to_account_info(),
|
|
154
|
+
],
|
|
155
|
+
)?;
|
|
156
|
+
|
|
157
|
+
let tile = &mut ctx.accounts.tile;
|
|
158
|
+
tile.owner = ctx.accounts.buyer.key();
|
|
159
|
+
tile.list_price = 0;
|
|
160
|
+
Ok(())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// B6 — SEEDED: lamports drained but data/discriminator left intact and owner
|
|
164
|
+
/// not reassigned — the account can be re-funded and revived with stale state.
|
|
165
|
+
pub fn close_tile(ctx: Context<CloseTile>) -> Result<()> {
|
|
166
|
+
let tile_info = &ctx.accounts.tile;
|
|
167
|
+
let dest = &ctx.accounts.owner;
|
|
168
|
+
|
|
169
|
+
let lamports = tile_info.lamports();
|
|
170
|
+
**tile_info.try_borrow_mut_lamports()? = 0;
|
|
171
|
+
**dest.try_borrow_mut_lamports()? += lamports;
|
|
172
|
+
Ok(())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// B7 + B8 — SEEDED: the program PDA-signs the transfer (so the runtime needs
|
|
176
|
+
/// no caller signature), `authority` is never required to sign, `recipient` is
|
|
177
|
+
/// caller-chosen, and `treasury` is never verified against the derived PDA.
|
|
178
|
+
/// Anyone can call this and drain the vault to themselves.
|
|
179
|
+
pub fn withdraw_treasury(ctx: Context<WithdrawTreasury>, amount: u64, bump: u8) -> Result<()> {
|
|
180
|
+
let ix = system_instruction::transfer(
|
|
181
|
+
&ctx.accounts.treasury.key(),
|
|
182
|
+
&ctx.accounts.recipient.key(),
|
|
183
|
+
amount,
|
|
184
|
+
);
|
|
185
|
+
invoke_signed(
|
|
186
|
+
&ix,
|
|
187
|
+
&[
|
|
188
|
+
ctx.accounts.treasury.to_account_info(),
|
|
189
|
+
ctx.accounts.recipient.to_account_info(),
|
|
190
|
+
],
|
|
191
|
+
&[&[b"treasury", &[bump]]],
|
|
192
|
+
)?;
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// B9 — SEEDED: profile is a raw account with no owner check; a fake profile
|
|
197
|
+
/// account owned by an attacker program passes straight through.
|
|
198
|
+
pub fn grant_achievement(ctx: Context<GrantAchievement>, xp: u64) -> Result<()> {
|
|
199
|
+
let profile_info = &ctx.accounts.profile;
|
|
200
|
+
let mut data = profile_info.try_borrow_mut_data()?;
|
|
201
|
+
// layout: [disc(8) | owner(32) | xp(8) | tiles_claimed(4)]
|
|
202
|
+
let current = u64::from_le_bytes(data[40..48].try_into().unwrap());
|
|
203
|
+
data[40..48].copy_from_slice(&(current + xp).to_le_bytes());
|
|
204
|
+
Ok(())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// C4 — clean control: checked math, has_one constraints.
|
|
208
|
+
pub fn harvest(ctx: Context<Harvest>) -> Result<()> {
|
|
209
|
+
let tile = &ctx.accounts.tile;
|
|
210
|
+
let profile = &mut ctx.accounts.profile;
|
|
211
|
+
let reward = (tile.level as u64)
|
|
212
|
+
.checked_mul(1_000)
|
|
213
|
+
.ok_or(GameError::Overflow)?;
|
|
214
|
+
profile.xp = profile.xp.checked_add(reward).ok_or(GameError::Overflow)?;
|
|
215
|
+
Ok(())
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn spl_token_transfer(
|
|
220
|
+
program_id: &Pubkey,
|
|
221
|
+
from: &Pubkey,
|
|
222
|
+
to: &Pubkey,
|
|
223
|
+
authority: &Pubkey,
|
|
224
|
+
amount: u64,
|
|
225
|
+
) -> Instruction {
|
|
226
|
+
let mut data = vec![3u8];
|
|
227
|
+
data.extend_from_slice(&amount.to_le_bytes());
|
|
228
|
+
Instruction {
|
|
229
|
+
program_id: *program_id,
|
|
230
|
+
accounts: vec![
|
|
231
|
+
AccountMeta::new(*from, false),
|
|
232
|
+
AccountMeta::new(*to, false),
|
|
233
|
+
AccountMeta::new_readonly(*authority, true),
|
|
234
|
+
],
|
|
235
|
+
data,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ------------------------- Accounts -------------------------
|
|
240
|
+
|
|
241
|
+
#[derive(Accounts)]
|
|
242
|
+
pub struct Initialize<'info> {
|
|
243
|
+
#[account(init, payer = authority, space = 8 + 32 + 8 + 8, seeds = [b"config"], bump)]
|
|
244
|
+
pub config: Account<'info, Config>,
|
|
245
|
+
/// CHECK: treasury PDA, system-owned vault
|
|
246
|
+
#[account(mut, seeds = [b"treasury"], bump)]
|
|
247
|
+
pub treasury: UncheckedAccount<'info>,
|
|
248
|
+
#[account(mut)]
|
|
249
|
+
pub authority: Signer<'info>,
|
|
250
|
+
pub system_program: Program<'info, System>,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[derive(Accounts)]
|
|
254
|
+
pub struct RegisterPlayer<'info> {
|
|
255
|
+
#[account(init_if_needed, payer = player, space = 8 + 32 + 8 + 4, seeds = [b"player", player.key().as_ref()], bump)]
|
|
256
|
+
pub profile: Account<'info, PlayerProfile>,
|
|
257
|
+
#[account(mut)]
|
|
258
|
+
pub player: Signer<'info>,
|
|
259
|
+
pub system_program: Program<'info, System>,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#[derive(Accounts)]
|
|
263
|
+
#[instruction(x: u8, y: u8)]
|
|
264
|
+
pub struct ClaimTile<'info> {
|
|
265
|
+
#[account(seeds = [b"config"], bump)]
|
|
266
|
+
pub config: Account<'info, Config>,
|
|
267
|
+
#[account(init, payer = player, space = 8 + 32 + 1 + 1 + 1 + 8, seeds = [b"tile", &[x], &[y]], bump)]
|
|
268
|
+
pub tile: Account<'info, Tile>,
|
|
269
|
+
#[account(mut, seeds = [b"player", player.key().as_ref()], bump)]
|
|
270
|
+
pub profile: Account<'info, PlayerProfile>,
|
|
271
|
+
/// CHECK: treasury PDA
|
|
272
|
+
#[account(mut, seeds = [b"treasury"], bump)]
|
|
273
|
+
pub treasury: UncheckedAccount<'info>,
|
|
274
|
+
#[account(mut)]
|
|
275
|
+
pub player: Signer<'info>,
|
|
276
|
+
pub system_program: Program<'info, System>,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[derive(Accounts)]
|
|
280
|
+
pub struct UpgradeTile<'info> {
|
|
281
|
+
#[account(mut, has_one = owner)]
|
|
282
|
+
pub tile: Account<'info, Tile>,
|
|
283
|
+
#[account(seeds = [b"player", owner.key().as_ref()], bump)]
|
|
284
|
+
pub profile: Account<'info, PlayerProfile>,
|
|
285
|
+
/// CHECK: treasury PDA — SEEDED B8-adjacent: no seeds binding here either
|
|
286
|
+
#[account(mut)]
|
|
287
|
+
pub treasury: UncheckedAccount<'info>,
|
|
288
|
+
#[account(mut)]
|
|
289
|
+
pub player: Signer<'info>,
|
|
290
|
+
/// CHECK: matches tile.owner via has_one
|
|
291
|
+
pub owner: UncheckedAccount<'info>,
|
|
292
|
+
pub system_program: Program<'info, System>,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[derive(Accounts)]
|
|
296
|
+
pub struct ListTile<'info> {
|
|
297
|
+
#[account(mut, has_one = owner)]
|
|
298
|
+
pub tile: Account<'info, Tile>,
|
|
299
|
+
pub owner: Signer<'info>,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[derive(Accounts)]
|
|
303
|
+
pub struct BuyTile<'info> {
|
|
304
|
+
/// CHECK: SEEDED B3 — raw account, no owner/discriminator validation
|
|
305
|
+
#[account(mut)]
|
|
306
|
+
pub tile: UncheckedAccount<'info>,
|
|
307
|
+
/// CHECK: SEEDED B3 — seller paid without cross-checking against tile state
|
|
308
|
+
#[account(mut)]
|
|
309
|
+
pub seller: UncheckedAccount<'info>,
|
|
310
|
+
/// CHECK: SEEDED — treasury never PDA-verified in this struct
|
|
311
|
+
#[account(mut)]
|
|
312
|
+
pub treasury: UncheckedAccount<'info>,
|
|
313
|
+
#[account(mut)]
|
|
314
|
+
pub buyer: Signer<'info>,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[derive(Accounts)]
|
|
318
|
+
pub struct BuyTileSpl<'info> {
|
|
319
|
+
#[account(mut, has_one = owner)]
|
|
320
|
+
pub tile: Account<'info, Tile>,
|
|
321
|
+
/// The official fee-token mint.
|
|
322
|
+
pub mint: Account<'info, Mint>,
|
|
323
|
+
/// SEEDED B5 — never bound to `mint` (token::mint = mint missing)
|
|
324
|
+
#[account(mut)]
|
|
325
|
+
pub buyer_token: Account<'info, TokenAccount>,
|
|
326
|
+
#[account(mut)]
|
|
327
|
+
pub seller_token: Account<'info, TokenAccount>,
|
|
328
|
+
#[account(mut)]
|
|
329
|
+
pub buyer: Signer<'info>,
|
|
330
|
+
/// CHECK: matches tile.owner via has_one
|
|
331
|
+
pub owner: UncheckedAccount<'info>,
|
|
332
|
+
/// CHECK: SEEDED B4 — CPI program id sourced from this unchecked account
|
|
333
|
+
pub token_program: UncheckedAccount<'info>,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[derive(Accounts)]
|
|
337
|
+
pub struct CloseTile<'info> {
|
|
338
|
+
/// CHECK: SEEDED B6 — manual close path
|
|
339
|
+
#[account(mut)]
|
|
340
|
+
pub tile: UncheckedAccount<'info>,
|
|
341
|
+
#[account(mut)]
|
|
342
|
+
pub owner: Signer<'info>,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[derive(Accounts)]
|
|
346
|
+
pub struct WithdrawTreasury<'info> {
|
|
347
|
+
/// CHECK: SEEDED B8 — never verified against the derived treasury PDA
|
|
348
|
+
#[account(mut)]
|
|
349
|
+
pub treasury: UncheckedAccount<'info>,
|
|
350
|
+
/// CHECK: SEEDED B7 — caller-chosen destination
|
|
351
|
+
#[account(mut)]
|
|
352
|
+
pub recipient: UncheckedAccount<'info>,
|
|
353
|
+
/// CHECK: SEEDED B7 — authority is never required to sign
|
|
354
|
+
pub authority: UncheckedAccount<'info>,
|
|
355
|
+
pub system_program: Program<'info, System>,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#[derive(Accounts)]
|
|
359
|
+
pub struct GrantAchievement<'info> {
|
|
360
|
+
/// CHECK: SEEDED B9 — raw profile account, no owner check
|
|
361
|
+
#[account(mut)]
|
|
362
|
+
pub profile: UncheckedAccount<'info>,
|
|
363
|
+
pub oracle: Signer<'info>,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[derive(Accounts)]
|
|
367
|
+
pub struct Harvest<'info> {
|
|
368
|
+
#[account(has_one = owner)]
|
|
369
|
+
pub tile: Account<'info, Tile>,
|
|
370
|
+
#[account(mut, seeds = [b"player", owner.key().as_ref()], bump)]
|
|
371
|
+
pub profile: Account<'info, PlayerProfile>,
|
|
372
|
+
pub owner: Signer<'info>,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ------------------------- State -------------------------
|
|
376
|
+
|
|
377
|
+
#[account]
|
|
378
|
+
pub struct Config {
|
|
379
|
+
pub authority: Pubkey,
|
|
380
|
+
pub claim_fee: u64,
|
|
381
|
+
pub royalty_bps: u64,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[account]
|
|
385
|
+
pub struct Tile {
|
|
386
|
+
pub owner: Pubkey,
|
|
387
|
+
pub x: u8,
|
|
388
|
+
pub y: u8,
|
|
389
|
+
pub level: u8,
|
|
390
|
+
pub list_price: u64,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[account]
|
|
394
|
+
pub struct PlayerProfile {
|
|
395
|
+
pub owner: Pubkey,
|
|
396
|
+
pub xp: u64,
|
|
397
|
+
pub tiles_claimed: u32,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[error_code]
|
|
401
|
+
pub enum GameError {
|
|
402
|
+
#[msg("Coordinates out of bounds")]
|
|
403
|
+
OutOfBounds,
|
|
404
|
+
#[msg("Tile is not listed for sale")]
|
|
405
|
+
NotForSale,
|
|
406
|
+
#[msg("Arithmetic overflow")]
|
|
407
|
+
Overflow,
|
|
408
|
+
#[msg("Profile mismatch")]
|
|
409
|
+
BadProfile,
|
|
410
|
+
}
|