Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ impl<'info> Contribute<'info> {
FundraiserError::ContributionTooBig
);

// Check if the fundraising duration has been reached
// Contributions are only accepted while the campaign is still open,
// i.e. before `duration` days have elapsed since it started.
let current_time = Clock::get()?.unix_timestamp;
require!(
self.fundraiser.duration <= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16,
(((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16) < self.fundraiser.duration,
crate::FundraiserError::FundraiserEnded
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ pub struct Refund<'info> {
impl<'info> Refund<'info> {
pub fn refund(&mut self) -> Result<()> {

// Check if the fundraising duration has been reached
// Refunds are only allowed once the campaign has ended, i.e. after
// `duration` days have elapsed since it started.
let current_time = Clock::get()?.unix_timestamp;

require!(
self.fundraiser.duration >= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16,
(((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16) >= self.fundraiser.duration,
crate::FundraiserError::FundraiserNotEnded
);

Expand Down
84 changes: 77 additions & 7 deletions tokens/token-fundraiser/anchor/tests/bankrun.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import * as anchor from "@anchor-lang/core";
import {
Expand All @@ -11,7 +12,7 @@ import {
import { PublicKey } from "@solana/web3.js";
import { BankrunProvider } from "anchor-bankrun";
import BN from "bn.js";
import { startAnchor } from "solana-bankrun";
import { Clock, startAnchor } from "solana-bankrun";
import IDL from "../target/idl/fundraiser.json";
import type { Fundraiser } from "../target/types/fundraiser";

Expand Down Expand Up @@ -51,6 +52,23 @@ describe("fundraiser bankrun", async () => {
return signature;
};

// Pin the bankrun clock to a deterministic point in the campaign so the
// day-based time gates are exercised regardless of the runtime's default
// clock behaviour: `days` days after the fundraiser's recorded start time.
const setElapsedDays = async (days: number) => {
const state = await program.account.fundraiser.fetch(fundraiser);
const clock = await context.banksClient.getClock();
context.setClock(
new Clock(
clock.slot,
clock.epochStartTimestamp,
clock.epoch,
clock.leaderScheduleEpoch,
BigInt(state.timeStarted.toString()) + BigInt(days * 24 * 60 * 60),
),
);
};

it("Test Preparation", async () => {
const airdrop = await provider.connection
.requestAirdrop(maker.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL)
Expand Down Expand Up @@ -82,7 +100,7 @@ describe("fundraiser bankrun", async () => {
const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);

const tx = await program.methods
.initialize(new BN(30000000), 0)
.initialize(new BN(30000000), 5)
.accountsPartial({
maker: maker.publicKey,
fundraiser,
Expand All @@ -98,6 +116,9 @@ describe("fundraiser bankrun", async () => {

console.log("\nInitialized fundraiser Account");
console.log("Your transaction signature", tx);

// Place "now" one day into the 5-day campaign so contributions are open.
await setElapsedDays(1);
});

it("Contribute to Fundraiser", async () => {
Expand Down Expand Up @@ -200,11 +221,44 @@ describe("fundraiser bankrun", async () => {
}
});

it("Refund Contributions", async () => {
it("Refund is rejected while the campaign is still open", async () => {
// One day into the 5-day campaign, so a refund must be rejected until it
// ends. (The successful-refund case advances the clock past `duration`.)
await setElapsedDays(1);
const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);

const contributorAccount = await program.account.contributor.fetch(contributor);
console.log("\nContributor balance", contributorAccount.amount.toString());
let rejected = false;
try {
await program.methods
.refund()
.accountsPartial({
contributor: provider.publicKey,
maker: maker.publicKey,
mintToRaise: mint,
fundraiser,
contributorAccount: contributor,
contributorAta: contributorATA,
vault,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc()
.then(confirm);
} catch (error) {
rejected = true;
assert.ok(String(error).includes("FundraiserNotEnded"), `expected a FundraiserNotEnded error, got: ${error}`);
}

assert.ok(rejected, "refund should be rejected while the campaign is still open");
});
Comment thread
dev-jodee marked this conversation as resolved.

it("Refunds the contributor after the campaign ends", async () => {
const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);

// Advance the clock past the 5-day campaign window so refunds are allowed.
await setElapsedDays(6);

const before = BigInt((await provider.connection.getTokenAccountBalance(contributorATA)).value.amount);

const tx = await program.methods
.refund()
Expand All @@ -223,7 +277,23 @@ describe("fundraiser bankrun", async () => {
.then(confirm);

console.log("\nRefunded contributions", tx);
console.log("Your transaction signature", tx);
console.log("Vault balance", (await provider.connection.getTokenAccountBalance(vault)).value.amount);

// The 2_000_000 contributed is returned to the contributor.
const after = BigInt((await provider.connection.getTokenAccountBalance(contributorATA)).value.amount);
assert.equal(after - before, BigInt(2_000_000), "contributor should be refunded their contribution");

// The vault is emptied and the contributor account is closed.
assert.equal(
(await provider.connection.getTokenAccountBalance(vault)).value.amount,
"0",
"vault should be empty after the refund",
);
let closed = false;
try {
await program.account.contributor.fetch(contributor);
} catch {
closed = true;
}
assert.ok(closed, "contributor account should be closed after the refund");
});
});