The Rustfund smart contract contains a High severity vulnerability where the withdraw function transfers funds before updating any state, potentially allowing a reentrancy attack through cross-program invocation (CPI).
In the withdraw() function, the contract transfers funds directly without updating any state that would prevent reentry:
While Solana's execution model provides some protection against reentrancy compared to other blockchains, cross-program invocation (CPI) could potentially exploit this pattern, especially as Solana evolves or in cross-chain interactions.
it("Has risk of reentrancy attack in withdraw function", async () => {
const reentrancyFundName = "Reentrancy Test Fund";
const [reentrancyFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(reentrancyFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(reentrancyFundName, description, goal)
.accounts({
fund: reentrancyFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const futureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 5);
await program.methods
.setDeadline(futureDeadline)
.accounts({
fund: reentrancyFundPDA,
creator: creator.publicKey,
})
.rpc();
const reentrancyContribution = new anchor.BN(50000000);
const [reentrancyContribPDA] = await PublicKey.findProgramAddress(
[reentrancyFundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(reentrancyContribution)
.accounts({
fund: reentrancyFundPDA,
contributor: provider.wallet.publicKey,
contribution: reentrancyContribPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(reentrancyFundPDA);
const fundBalanceBefore = await provider.connection.getBalance(reentrancyFundPDA);
console.log("Waiting for deadline to pass (6 seconds)...");
await new Promise((resolve) => setTimeout(resolve, 6000));
try {
await program.methods
.withdraw()
.accounts({
fund: reentrancyFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Withdrawal successful in reentrancy test");
const fundBalanceAfter = await provider.connection.getBalance(reentrancyFundPDA);
reportBug(
"HIGH",
"Reentrancy Attack Risk",
"The withdraw function transfers funds before updating state, creating a potential reentrancy vulnerability",
`Fund balance before withdrawal: ${fundBalanceBefore}\n` +
`Fund balance after withdrawal: ${fundBalanceAfter}\n\n` +
`Code issue: In lib.rs:120-123, the contract transfers funds with no state updates to prevent reentrancy:\n` +
`\`\`\`rust\n` +
`**fund.to_account_info().try_borrow_mut_lamports()? -= withdraw_amount;\n` +
`**creator.to_account_info().try_borrow_mut_lamports()? += withdraw_amount;\n` +
`\`\`\`\n\n` +
`There is no state flag set to prevent reentrancy. While Solana's execution model provides some protection,\n` +
`cross-program invocation (CPI) could still potentially be exploited by a malicious contract.\n\n` +
`Best practice: Update state before transferring funds by adding a withdraw_in_progress flag.`
);
} catch (e) {
console.log("Withdrawal failed despite passed deadline:", e);
reportBug(
"HIGH",
"Reentrancy Attack Risk",
"The withdraw function lacks proper state updates before transferring funds",
`Fund balance: ${fundBalanceBefore}\n\n` +
`Code analysis: While we couldn't execute the withdraw function to demonstrate the issue,\n` +
`analysis of lib.rs:120-123 shows a potential vulnerability with direct fund transfers without state updates:\n\n` +
`\`\`\`rust\n` +
`**fund.to_account_info().try_borrow_mut_lamports()? -= withdraw_amount;\n` +
`**creator.to_account_info().try_borrow_mut_lamports()? += withdraw_amount;\n` +
`\`\`\`\n\n` +
`Without a withdraw_in_progress flag or other state tracking, a reentrant call through CPI could\n` +
`potentially exploit this pattern in future versions of Solana or cross-chain interactions.`
);
}
});