RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Invalid

State Inconsistency Due to Late State Update (Logic Flaw + Potential Fund Loss)

[H-2] State Inconsistency Due to Late State Update (Logic Flaw + Potential Fund Loss)

Description:

In the original refund function, the contribution.amount is reset to 0 after transferring lamports from the fund to the contributor. If the transaction fails (e.g., due to insufficient funds or an overflow) after the transfers but before the reset, the state becomes inconsistent: the contributor receives funds, but contribution.amount remains non-zero, potentially allowing a double refund.

Impact:

This could lead to financial loss for the program, as a contributor might repeatedly claim refunds for the same contribution, exploiting the inconsistency. It also complicates auditing and state tracking.

Proof of Concept:

  1. Contributor calls refund with contribution.amount = 1000.

  2. Lamports are transferred: fund loses 1000, contributor gains 1000.

  3. Transaction fails (e.g., checked_add overflows) after transfers but before contribution.amount = 0.

  4. State shows contribution.amount = 1000, despite funds being moved.

  5. Contributor calls refund again, claiming another 1000 lamports.

const anchor = require("@project-serum/anchor");
const { PublicKey, SystemProgram } = anchor.web3;
const assert = require("assert");
describe("State Inconsistency", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Rustfund;
it("Demonstrates state inconsistency on failure", async () => {
const fundKp = anchor.web3.Keypair.generate();
const contributorKp = anchor.web3.Keypair.generate();
const [contributionPda] = await PublicKey.findProgramAddress(
[fundKp.publicKey.toBuffer(), contributorKp.publicKey.toBuffer()],
program.programId
);
// Setup: Create fund and contribute
await program.rpc.fundCreate("test", "desc", new anchor.BN(2000), {
accounts: {
fund: fundKp.publicKey,
creator: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [fundKp],
});
await provider.connection.requestAirdrop(fundKp.publicKey, 2000);
await program.rpc.contribute(new anchor.BN(1000), {
accounts: {
fund: fundKp.publicKey,
contributor: contributorKp.publicKey,
contribution: contributionPda,
systemProgram: SystemProgram.programId,
},
signers: [contributorKp],
});
// Simulate original refund with forced failure (overflow)
try {
await program.rpc.refund({
accounts: {
fund: fundKp.publicKey,
contribution: contributionPda,
contributor: contributorKp.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [contributorKp],
instructions: [
// Simulate failure after transfer by exceeding lamports
SystemProgram.transfer({
fromPubkey: contributorKp.publicKey,
toPubkey: fundKp.publicKey,
lamports: 2 ** 64, // Overflow
}),
],
});
} catch (err) {
const contribution = await program.account.contribution.fetch(contributionPda);
assert.equal(contribution.amount.toNumber(), 1000, "Contribution amount not reset");
const fundBalance = await provider.connection.getBalance(fundKp.publicKey);
assert.equal(fundBalance, 1000, "Funds transferred despite failure");
}
});
});

Recommended Mitigation:
Update state before transfers:

contribution.amount = 0;
**fund.to_account_info().try_borrow_mut_lamports()? =
fund.to_account_info().lamports().checked_sub(amount).ok_or(ProgramError::InsufficientFunds)?;
**contributor.to_account_info().try_borrow_mut_lamports()? =
contributor.to_account_info().lamports().checked_add(amount).ok_or(ErrorCode::CalculationOverflow)?;
Updates

Lead Judging Commences

bube Lead Judge
2 months ago

Appeal created

bube Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.