Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 168 additions & 96 deletions tokens/token-swap/anchor/tests/swap.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,168 @@
import type { Program } from "@anchor-lang/core";
import * as anchor from "@anchor-lang/core";
import { BN } from "bn.js";
import { expect } from "chai";
import type { SwapExample } from "../target/types/swap_example";
import { createValues, mintingTokens, type TestValues } from "./utils";

describe("Swap", () => {
const provider = anchor.AnchorProvider.env();
const connection = provider.connection;
anchor.setProvider(provider);

const program = anchor.workspace.SwapExample as Program<SwapExample>;

let values: TestValues;

beforeEach(async () => {
values = createValues();

await program.methods
.createAmm(values.id, values.fee)
.accounts({ amm: values.ammKey, admin: values.admin.publicKey })
.rpc();

await mintingTokens({
connection,
creator: values.admin,
mintAKeypair: values.mintAKeypair,
mintBKeypair: values.mintBKeypair,
});

await program.methods
.createPool()
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
mintLiquidity: values.mintLiquidity,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
})
.rpc();

await program.methods
.depositLiquidity(values.depositAmountA, values.depositAmountB)
.accounts({
pool: values.poolKey,
poolAuthority: values.poolAuthority,
depositor: values.admin.publicKey,
mintLiquidity: values.mintLiquidity,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
depositorAccountLiquidity: values.liquidityAccount,
depositorAccountA: values.holderAccountA,
depositorAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc({ skipPreflight: true });
});

it("Swap from A to B", async () => {
const input = new BN(10 ** 6);
await program.methods
.swapExactTokensForTokens(true, input, new BN(100))
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
trader: values.admin.publicKey,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
traderAccountA: values.holderAccountA,
traderAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc({ skipPreflight: true });

const traderTokenAccountA = await connection.getTokenAccountBalance(values.holderAccountA);
const traderTokenAccountB = await connection.getTokenAccountBalance(values.holderAccountB);
expect(traderTokenAccountA.value.amount).to.equal(
values.defaultSupply.sub(values.depositAmountA).sub(input).toString(),
);
expect(Number(traderTokenAccountB.value.amount)).to.be.greaterThan(
values.defaultSupply.sub(values.depositAmountB).toNumber(),
);
expect(Number(traderTokenAccountB.value.amount)).to.be.lessThan(
values.defaultSupply.sub(values.depositAmountB).add(input).toNumber(),
);
});
});
import type { Program } from "@anchor-lang/core";
import * as anchor from "@anchor-lang/core";
import { BN } from "bn.js";
import { expect } from "chai";
import type { SwapExample } from "../target/types/swap_example";
import { createValues, mintingTokens, type TestValues } from "./utils";

describe("Swap", () => {
const provider = anchor.AnchorProvider.env();
const connection = provider.connection;
anchor.setProvider(provider);

const program = anchor.workspace.SwapExample as Program<SwapExample>;

let values: TestValues;

beforeEach(async () => {
values = createValues();

await program.methods
.createAmm(values.id, values.fee)
.accounts({ amm: values.ammKey, admin: values.admin.publicKey })
.rpc();

await mintingTokens({
connection,
creator: values.admin,
mintAKeypair: values.mintAKeypair,
mintBKeypair: values.mintBKeypair,
});

await program.methods
.createPool()
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
mintLiquidity: values.mintLiquidity,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
})
.rpc();

await program.methods
.depositLiquidity(values.depositAmountA, values.depositAmountB)
.accounts({
pool: values.poolKey,
poolAuthority: values.poolAuthority,
depositor: values.admin.publicKey,
mintLiquidity: values.mintLiquidity,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
depositorAccountLiquidity: values.liquidityAccount,
depositorAccountA: values.holderAccountA,
depositorAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc({ skipPreflight: true });
});

it("Swap from A to B", async () => {
const input = new BN(10 ** 6);
await program.methods
.swapExactTokensForTokens(true, input, new BN(100))
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
trader: values.admin.publicKey,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
traderAccountA: values.holderAccountA,
traderAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc({ skipPreflight: true });

const traderTokenAccountA = await connection.getTokenAccountBalance(values.holderAccountA);
const traderTokenAccountB = await connection.getTokenAccountBalance(values.holderAccountB);
expect(traderTokenAccountA.value.amount).to.equal(
values.defaultSupply.sub(values.depositAmountA).sub(input).toString(),
);
expect(Number(traderTokenAccountB.value.amount)).to.be.greaterThan(
values.defaultSupply.sub(values.depositAmountB).toNumber(),
);
expect(Number(traderTokenAccountB.value.amount)).to.be.lessThan(
values.defaultSupply.sub(values.depositAmountB).add(input).toNumber(),
);
});

it("Withdraw Liquidity successfully", async () => {
const initialLiquidity = await connection.getTokenAccountBalance(values.liquidityAccount);
const initialA = await connection.getTokenAccountBalance(values.holderAccountA);
const initialB = await connection.getTokenAccountBalance(values.holderAccountB);

const withdrawAmount = new BN(initialLiquidity.value.amount).div(new BN(2));

await program.methods
.withdrawLiquidity(withdrawAmount)
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
depositor: values.admin.publicKey,
mintLiquidity: values.mintLiquidity,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
depositorAccountLiquidity: values.liquidityAccount,
depositorAccountA: values.holderAccountA,
depositorAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc();

const postLiquidity = await connection.getTokenAccountBalance(values.liquidityAccount);
const postA = await connection.getTokenAccountBalance(values.holderAccountA);
const postB = await connection.getTokenAccountBalance(values.holderAccountB);

expect(new BN(postLiquidity.value.amount).toString()).to.equal(
new BN(initialLiquidity.value.amount).sub(withdrawAmount).toString()
);
Comment on lines +123 to +129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Withdraw test asserts LP burn but not underlying token receipt

The test only checks that the LP token balance drops by withdrawAmount. It never checks that holderAccountA or holderAccountB balances increased. A regression where the program burns LP tokens but fails to transfer the underlying tokens back would pass this test without notice.


expect(new BN(postA.value.amount).gt(new BN(initialA.value.amount))).to.be.true;
expect(new BN(postB.value.amount).gt(new BN(initialB.value.amount))).to.be.true;
});

it("Should fail if slippage limit is violated (min_output_amount too high)", async () => {
const input = new BN(10 ** 6);
const impossibleMinOutput = input.mul(new BN(10));

let targetError: any = null;

try {
await program.methods
.swapExactTokensForTokens(true, input, impossibleMinOutput)
.accounts({
amm: values.ammKey,
pool: values.poolKey,
poolAuthority: values.poolAuthority,
trader: values.admin.publicKey,
mintA: values.mintAKeypair.publicKey,
mintB: values.mintBKeypair.publicKey,
poolAccountA: values.poolAccountA,
poolAccountB: values.poolAccountB,
traderAccountA: values.holderAccountA,
traderAccountB: values.holderAccountB,
})
.signers([values.admin])
.rpc();
} catch (err: any) {
targetError = err;
}

if (!targetError) {
expect.fail("The transaction should have failed due to high slippage bounds, but it succeeded.");
}
Comment on lines +158 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Catch block validates error existence only, not error type

expect(err).to.exist is always satisfied by any thrown value, including network errors, account-not-found errors, or the AssertionError from expect.fail. The test won't distinguish a real slippage rejection from an unrelated setup failure. Consider asserting the specific program error code (e.g., err.error?.errorCode?.code === "SlippageExceeded") to make sure the right error is being raised.

const errorMessage = targetError.toString();
expect(errorMessage).to.include("OutputTooSmall");
});
});