Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The atomic sandwich: a locked pool is no boundary against the authority

Source: crates/amm-frood-test/tests/sandwich.rs L170

  • Given: Mallory holds the pool authority; Alice is an honest LP
  • When: the pool is initialized with Mallory as authority
  • When: Alice deposits liquidity
  • When: Mallory freezes the pool: trading is publicly disabled
  • When: anyone tries to swap the frozen pool
  • Then: a swap against the locked pool is refused ✓
  • When: Mallory unlocks, swaps, and re-locks in one transaction
  • Then: Mallory traded despite the lock (X was spent) ✓
  • Then: config.locked = true ✓

Result: ✓ success — 31775 CU · claims 3/3 ✓

sequenceDiagram
    participant p0 as Mallory
    participant p1 as Amm
    participant p2 as token
    p0->>p1: SetLocked
    activate p1
    p1-->>p0: ✓ 4081cu
    deactivate p1
    p0->>p1: Swap
    activate p1
    p1->>p2: transferChecked
    activate p2
    p2-->>p1: ✓ 105cu
    deactivate p2
    p1->>p2: transferChecked
    activate p2
    p2-->>p1: ✓ 105cu
    deactivate p2
    p1-->>p0: ✓ 23615cu
    deactivate p1
    p0->>p1: SetLocked
    activate p1
    p1-->>p0: ✓ 4079cu
    deactivate p1

Authority

flowchart LR
    Amm["Amm"]:::program
    Mallory(["Mallory"]):::signer
    Pool(["Pool"]):::signer
    vaultXPoolMint[("vaultX(Pool, Mint)")]:::state
    vaultYPoolMint2[("vaultY(Pool, Mint 2)")]:::state
    userXMalloryMint[("userX(Mallory, Mint)")]:::state
    userYMalloryMint2[("userY(Mallory, Mint 2)")]:::state
    token["token"]:::program
    Mallory -->|signs| Amm
    Amm -->|writes| Pool
    Amm -->|writes| vaultXPoolMint
    Amm -->|writes| vaultYPoolMint2
    Amm -->|writes| userXMalloryMint
    Amm -->|writes| userYMalloryMint2
    token -->|writes| userXMalloryMint
    token -->|writes| vaultXPoolMint
    Mallory -->|signs| token
    token -->|writes| vaultYPoolMint2
    token -->|writes| userYMalloryMint2
    Pool -->|signs| token
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Ownership

flowchart LR
    system["system"]:::program
    Mallory[("Mallory")]:::state
    Amm["Amm"]:::program
    Pool[("Pool")]:::state
    token["token"]:::program
    vaultXPoolMint[("vaultX(Pool, Mint)")]:::state
    vaultYPoolMint2[("vaultY(Pool, Mint 2)")]:::state
    userXMalloryMint[("userX(Mallory, Mint)")]:::state
    userYMalloryMint2[("userY(Mallory, Mint 2)")]:::state
    system -->|owns| Mallory
    Amm -->|owns| Pool
    token -->|owns| vaultXPoolMint
    token -->|owns| vaultYPoolMint2
    token -->|owns| userXMalloryMint
    token -->|owns| userYMalloryMint2
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Accounts

AccountSignerWritableOwner
Mallorysystem
Pool·Amm
Mint··token
Mint 2··token
vaultX(Pool, Mint)·token
vaultY(Pool, Mint 2)·token
userX(Mallory, Mint)·token
userY(Mallory, Mint 2)·token
token··BPFL…
Call tree and logs
The atomic sandwich: a locked pool is no boundary against the authority
Mallory (31775cu)
├─ Amm::SetLocked ✓ 4081cu
├─ Amm::Swap ✓ 23615cu
│  ├─ token::transferChecked ✓ 105cu
│  └─ token::transferChecked ✓ 105cu
└─ Amm::SetLocked ✓ 4079cu
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi invoke [1]
Program log: Instruction: SetLocked
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi consumed 4081 of 600000 compute units
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi success
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi invoke [1]
Program log: Instruction: Swap
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 574904 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 572931 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi consumed 23615 of 595919 compute units
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi success
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi invoke [1]
Program log: Instruction: SetLocked
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi consumed 4079 of 572304 compute units
Program 5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi success

Integration

What an integrator writes: the derivations and the instruction, byte-identical to the transaction this page witnessed. Addresses are this scenario’s; supply your own.

#![allow(unused)]
fn main() {
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::{pubkey, Pubkey};

// --- setLocked ---
let program_id: Pubkey = pubkey!("5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi");
let authority_1: Pubkey = pubkey!("2vG6xpsbeMe9s2VBYroRrjYzkaPzJXbh8Pn24UfMGuzx"); // Mallory: this scenario's value; supply your own
let config_1: Pubkey = pubkey!("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"); // Pool: this scenario's value; supply your own
let ix_1 = Instruction {
    program_id,
    accounts: vec![
        AccountMeta::new_readonly(authority_1, true), // authority
        AccountMeta::new(config_1, false), // config
    ],
    // discriminator ++ borsh(locked: false)
    data: vec![0x9d, 0x03, 0x30, 0x38, 0x1f, 0xd4, 0xf2, 0x58, 0x00],
};

// --- swap ---
let program_id: Pubkey = pubkey!("5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi");
let user_2: Pubkey = pubkey!("2vG6xpsbeMe9s2VBYroRrjYzkaPzJXbh8Pn24UfMGuzx"); // Mallory: this scenario's value; supply your own
let mint_x_2: Pubkey = pubkey!("Dq26t7JZEn9jCpma5Cn77d76SECZ79m2xwxpDDVnf6jn"); // Mint: this scenario's value; supply your own
let mint_y_2: Pubkey = pubkey!("DAxYEAkg7hsB7qZA8bLEJJPkNEgtEYqbxjcZx1VfLK5k"); // Mint 2: this scenario's value; supply your own
let config_2: Pubkey = pubkey!("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"); // Pool: this scenario's value; supply your own
let token_program_2: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // the token program
let vault_x_2: Pubkey = pubkey!("BEmMWKwvGZMBG47GzsU1noJYpgRtRAmJAjKzxmYAr1PY"); // derivation elided (a seed's encoding is beyond this snippet); address from the run
let vault_y_2: Pubkey = pubkey!("4dGtYZgpFMtNGJEpckGCvwYZs9VxYa1j3hPaeDsMejjy"); // derivation elided (a seed's encoding is beyond this snippet); address from the run
let user_x_2: Pubkey = pubkey!("G4yBZJK59Dvd4PZpL12d7BFZyL7GY6G2n3Y32o34BXG9"); // derivation elided (a seed's encoding is beyond this snippet); address from the run
let user_y_2: Pubkey = pubkey!("9YfuNPSd8wuBPh2FcizzSMK4A8dWNjApLjRkHwL5FGMB"); // derivation elided (a seed's encoding is beyond this snippet); address from the run
let ix_2 = Instruction {
    program_id,
    accounts: vec![
        AccountMeta::new(user_2, true), // user
        AccountMeta::new_readonly(mint_x_2, false), // mintX
        AccountMeta::new_readonly(mint_y_2, false), // mintY
        AccountMeta::new_readonly(config_2, false), // config
        AccountMeta::new(vault_x_2, false), // vaultX
        AccountMeta::new(vault_y_2, false), // vaultY
        AccountMeta::new(user_x_2, false), // userX
        AccountMeta::new(user_y_2, false), // userY
        AccountMeta::new_readonly(token_program_2, false), // tokenProgram
    ],
    // discriminator ++ borsh(kind: …, aToB: true)
    data: vec![0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
};

// --- setLocked ---
let program_id: Pubkey = pubkey!("5aDxxnPDGeVEuLXerisV4GF5f8tcwTjbMK7Bn1dYUXSi");
let authority_3: Pubkey = pubkey!("2vG6xpsbeMe9s2VBYroRrjYzkaPzJXbh8Pn24UfMGuzx"); // Mallory: this scenario's value; supply your own
let config_3: Pubkey = pubkey!("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"); // Pool: this scenario's value; supply your own
let ix_3 = Instruction {
    program_id,
    accounts: vec![
        AccountMeta::new_readonly(authority_3, true), // authority
        AccountMeta::new(config_3, false), // config
    ],
    // discriminator ++ borsh(locked: true)
    data: vec![0x9d, 0x03, 0x30, 0x38, 0x1f, 0xd4, 0xf2, 0x58, 0x01],
};
}

The same call through the Codama-generated TypeScript client (the async builder derives every PDA the resolver derived here, so only the free inputs appear):

import { address } from "@solana/kit";
import { getSetLockedInstructionAsync } from "your-generated-client";

const ix = await getSetLockedInstructionAsync({
  authority: authoritySigner, // Mallory (a TransactionSigner)
  config: address("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"), // Pool
  locked: false,
});
import { getSwapInstructionAsync } from "your-generated-client";

const ix = await getSwapInstructionAsync({
  user: userSigner, // Mallory (a TransactionSigner)
  mintX: address("Dq26t7JZEn9jCpma5Cn77d76SECZ79m2xwxpDDVnf6jn"), // Mint
  mintY: address("DAxYEAkg7hsB7qZA8bLEJJPkNEgtEYqbxjcZx1VfLK5k"), // Mint 2
  config: address("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"), // Pool
  // tokenProgram defaults to the token program
  kind: { __kind: "exactInput" },
  aToB: true,
});
import { getSetLockedInstructionAsync } from "your-generated-client";

const ix = await getSetLockedInstructionAsync({
  authority: authoritySigner, // Mallory (a TransactionSigner)
  config: address("DWm4MrK6YroDTYNXjcpjh7k5TLbriQDfkQjePWuAmLVc"), // Pool
  locked: true,
});