fix(market,referrals): reorder effects before interaction and add a reentrancy guard - #133
fix(market,referrals): reorder effects before interaction and add a reentrancy guard#133wagmiiii wants to merge 1 commit into
Conversation
…eentrancy guard place_bet performed its external calls (XLM transfer, referral fee transfer, and the credit() cross-call) before writing BetEntry, the bettor index, and market totals — the classic check-effects-interaction violation described in SPulse-Org#72. Reorder so all state that does not depend on credit()'s result is written before any external call; only the referral portion of the fee accumulator is finalized after credit() returns. Add a single-bool reentrancy guard to the state-mutating entry points that perform external calls in both prediction_market (place_bet, claim, cancel_refund, withdraw_fees, execute_withdraw_fees) and referral_registry (register_referral, credit). Soroban's host already rejects cross-contract re-entry at the protocol level; the guard is defense-in-depth and rejects the call with a dedicated ReentrancyDetected error before any partially-updated state can be observed. credit() also now records ReferralEarnings before transferring XLM to the referrer and invoking the leaderboard, closing the same ordering gap. Closes SPulse-Org#72
b446231 to
0867ebf
Compare
Muyideen-js
left a comment
There was a problem hiding this comment.
@wagmiiii This PR does not fully resolve issue #72. While it adds a reentrancy guard and reorders some state writes, the core vulnerability remains: the credit() function still transfers XLM to an arbitrary referrer address before the market's state is fully finalized. The reentrancy guard is ineffective because Soroban already prohibits cross-contract reentrancy at the protocol level, so the guard never triggers in practice. The added test only confirms that Soroban's host rejects reentrancy, not that the guard works. To properly fix this, you need to eliminate external calls to untrusted addresses before all state is committed. Consider redesigning the fee flow to avoid calling credit() before finalizing the market state, or require referrers to be whitelisted/trusted. Additionally, ensure that all state mutations happen before any external call, including the referral fee accumulation. Please revise the approach to address the root cause.
Muyideen-js
left a comment
There was a problem hiding this comment.
@wagmiiii This PR does not solve the critical issue. The reentrancy guard is redundant because Soroban already prohibits cross-contract reentrancy (as the test itself shows). The CEI reordering in place_bet still leaves the referral fee accumulator update after the external credit() call, which is exactly the pattern the issue says is unsafe. The issue explicitly states that moving state writes before external calls conflicts with the fee flow, and this PR does not resolve that conflict. The test added expects a host error, not the guard's error, so it does not demonstrate the guard works. To fix this, you must redesign the bet/fee flow so that no external call to an untrusted address occurs before all state is finalized. Consider deferring the referral fee transfer until after the bet is fully recorded, or use a pull-based referral fee system. The current patch is insufficient and does not close the vulnerability.
Muyideen-js
left a comment
There was a problem hiding this comment.
@wagmiiii This PR does not fully solve issue #72. The reentrancy guard is ineffective because Soroban already prohibits cross-contract reentrancy, as your own test shows. The CEI reordering is incomplete: the XLM transfer to the referrer and the credit() call still occur before the market state is fully finalized (e.g., AccumulatedFees is updated after the transfer). An attacker can still exploit the arbitrary referrer to reenter claim or cancel_refund during place_bet, observing partially-updated state. To fix this, you must redesign the fee flow to ensure all state is written before any external call, or eliminate the external call to untrusted referrers entirely (e.g., by deferring the transfer or using a pull-based model). Also, provide a test that demonstrates the attack is prevented, and ensure CI passes.
Muyideen-js
left a comment
There was a problem hiding this comment.
@wagmiiii This PR does not fully resolve issue #72. The core problem is that credit() transfers XLM to an arbitrary referrer address before the market state is finalized. While you added a reentrancy guard and reordered some effects, the referral fee transfer still occurs before the AccumulatedFees is fully updated (the referral portion is added after the transfer). Moreover, the guard is ineffective because Soroban's host already prohibits cross-contract reentry, as you noted. The guard does not prevent a malicious referrer contract from reentering other contracts (e.g., leaderboard) or performing other actions that observe partially-updated state. The test only checks that reentry into place_bet is rejected by the host, not the actual attack vector. To fix this, you must ensure that no external calls are made to untrusted addresses before all state is finalized. Consider deferring the referral fee transfer until after the bet is fully recorded, or use a pull-based payment pattern where the referrer claims their fee later. Also, add a test that simulates the exact attack: a malicious referrer reentering claim or cancel_refund from within credit(). Please revise accordingly.
Muyideen-js
left a comment
There was a problem hiding this comment.
@wagmiiii This PR does not fully solve the issue. The core problem is that credit() transfers XLM to an arbitrary referrer before the market state is finalized. Adding a reentrancy guard is ineffective because Soroban already prohibits reentrancy at the protocol level, and the guard is never triggered in practice. The CEI reordering is incomplete: the referral fee is still added to AccumulatedFees after the external transfer, leaving a window where state is inconsistent. To fix this, you must either (1) move the referral fee transfer after all state is written, or (2) remove the arbitrary referrer transfer entirely and use a trusted referral contract. Additionally, the test should verify that the guard's ReentrancyDetected error is returned, not just rely on Soroban's host-level prohibition. Please revise the approach to eliminate the external call to untrusted addresses before state finalization.
Problem
place_betperformed its external calls (XLM transfer, referral-fee transfer, and thecredit()cross-call) before writingBetEntry, the bettor index, and market totals — the classic check-effects-interaction violation.credit()similarly recordedReferralEarningsonly after transferring XLM to the referrer and invoking the leaderboard.Fix
place_bet: all state that does not depend oncredit()'s result is written before any external call. Only the referral portion of the fee accumulator is finalized aftercredit()returns (it's unknowable until then).place_bet,claim,cancel_refund,withdraw_fees,execute_withdraw_fees;register_referral,credit) and cleared on success.credit()reordering:ReferralEarningsis recorded before the external transfer/leaderboard call.withdraw_fees()reordering: the accumulator is zeroed before the external transfer.Tests
Added
test_place_bet_rejects_reentrancy: a malicious referral contract re-entersplace_betfrom insidecredit(); the nested call is rejected and the whole transaction reverts.Closes #72