Deposit Fee

QuantumRaffle V2 supports an optional host-configurable deposit fee that is skimmed from the entry revenue of every inbound ETH deposit. Entries are priced and dust is refunded first; the fee is then taken from the resulting effectiveValue before the pool splits. The fee is locked into a game's config when the game starts and cannot be changed mid-raffle.

Properties

Property Value
Storage gameConfig.depositFeeBps (per-game; copied into gameConfigRecord[gameId] when the game starts)
Units Basis points (1 bp = 0.01%)
Default 0 (disabled — contract behaves as before)
Maximum MAX_DEPOSIT_FEE_BPS = 1000 (10%)
Set by Host only, via constructor or setNextGameConfig(...) (affects the next game)
Withdrawn by Host only, via withdrawDepositFees() (any time, survives across games)
Accrued in accruedDepositFees (uint256, claimable any time)

Why the fee is locked at game start

depositFeeBps is part of the per-game gameConfig struct, alongside entryAmount, deadline, the log bases, and the proportions. When a host starts a new game, the contract snapshots nextGameConfig into gameConfigRecord[gameId]. Every deposit during that game reads gameConfigRecord[gameId].depositFeeBps, so the rate that was in effect at game start is the rate every participant pays for the entire raffle. The host cannot raise or lower the fee on an active game — setNextGameConfig only updates the parameters of the next game.

This is a deliberate guarantee: a participant who enters a game knows exactly what fee they will pay, and that rate is part of the same GameStarted and GameConfigUpdated events that already announce the rest of the game's parameters.

Where the fee is captured

receive() computes entries from msg.value first, then skims the fee from the resulting entry revenue. The fee does not inflate the entry price — it reduces what the pools receive. The rate is read from the active game's locked config:

gameConfig memory config = gameConfigRecord[gameId]; // locked at game start

uint256 num_entries    = msg.value / config.entryAmount;
uint256 refundAmount   = msg.value % config.entryAmount;
uint256 effectiveValue = msg.value - refundAmount;

if (num_entries == 0) revert NoValidEntries();

uint256 feeAmount        = effectiveValue * config.depositFeeBps / BPS_DENOMINATOR;
uint256 poolContribution = effectiveValue - feeAmount;

if (feeAmount > 0) {
    accruedDepositFees += feeAmount;
    emit DepositFeeCollected(gameId, msg.sender, feeAmount, poolContribution);
}

// Pool splits run on poolContribution (effectiveValue - feeAmount)

Entry count, refund, and participantRecord are computed from effectiveValue directly — the fee does not affect them. The grand-prize pool and adoption-bonus pools receive poolContribution instead of effectiveValue, so the fee reduces participant winnings proportionally.

What the sender experiences

An entry always costs exactly entryAmount wei. A player who wants k entries sends exactly k * entryAmount wei regardless of the fee rate.

For a 1% fee and 1 entry of 1 ETH:

Value
Sender sends 1 ETH
num_entries 1
effectiveValue 1 ETH
refundAmount 0
feeAmount 0.01 ETH → accrues to host
poolContribution 0.99 ETH → split between pools
Net spend 1 ETH (same as fee-free case)

The difference vs. a 0% fee game is where the 1 ETH ends up — the player always pays 1 ETH, but 0.01 ETH of that flows to accruedDepositFees instead of the prize/bonus pools.

If the sender does not send enough for at least one entry, the transaction reverts with NoValidEntries before the fee is computed, so no fee is captured from a deposit that fails to purchase at least one entry.

Withdrawal

The current host can call withdrawDepositFees() at any time to sweep accruedDepositFees to their address. The function:

  • Reverts with OnlyHost if called by anyone other than the current host
  • Resets accruedDepositFees to 0 before sending (CEI pattern)
  • Emits DepositFeesWithdrawn(host, amount)
  • Is a no-op (with event) if accruedDepositFees == 0

Interaction with host transfer

Accrued fees follow the host role: after a two-step host transfer, the new host withdraws any fees that were accrued under the old host. This is intentional — the contract never sends fees to a stale address.

Events and errors

Event When
GameStarted(..., depositFeeBps) A new game starts; the locked fee is included as the trailing field
GameConfigUpdated(..., depositFeeBps) Host updates nextGameConfig; the new fee applies to the next game
DepositFeeCollected(gameId, payer, feeAmount, poolContribution) Fee captured on a successful deposit (only when feeAmount > 0)
DepositFeesWithdrawn(host, amount) Host withdraws accrued fees
Error Cause
DepositFeeTooHigh Constructor or setNextGameConfig called with depositFeeBps > MAX_DEPOSIT_FEE_BPS
OnlyHost Non-host calls setNextGameConfig or withdrawDepositFees

Backwards compatibility

When the constructor is called with depositFeeBps = 0 and the host never raises it, the contract behaves identically to a version without the fee feature: no DepositFeeCollected event is emitted, accruedDepositFees stays at 0, and prize-pool math is unchanged.

results matching ""

    No results matching ""