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
OnlyHostif called by anyone other than the current host - Resets
accruedDepositFeesto0before 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.