Skip to main content

EmberLottery.sol

Audit Metadata


🔬 Analyzer Technical Report

Gas Optimizations

IssueInstances
GAS-1a = a + b is more gas effective than a += b for state variables (excluding arrays and mappings)2
GAS-2Use assembly to check for address(0)3
GAS-3State variables should be cached in stack variables rather than re-reading them from storage2
GAS-4For Operations that will not overflow, you could use unchecked18
GAS-5Use Custom Errors instead of Revert Strings to save Gas1
GAS-6Functions guaranteed to revert when called by normal users can be marked payable2
GAS-7++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1)2
GAS-8Using private rather than public for constants, saves gas3
GAS-9Increments/decrements can be unchecked in for-loops1
GAS-10Use != 0 instead of > 0 for unsigned integer comparison5

[GAS-1] a = a + b is more gas effective than a += b for state variables (excluding arrays and mappings)

This saves 16 gas per instance.

Instances (2):

File: EmberLottery.sol

134: ticketCount[currentLotteryId][msg.sender] += _ticketCount;

135: lottery.totalPot += cost;

Link to code

[GAS-2] Use assembly to check for address(0)

Saves 6 gas per instance

Instances (3):

File: EmberLottery.sol

68: if (_feeRecipient == address(0)) revert ZeroAddress();

170: if (storedCommit == bytes32(0)) revert InvalidCommit();

267: if (_feeRecipient == address(0)) revert ZeroAddress();

Link to code

[GAS-3] State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read.

Saves 100 gas per instance

Instances (2):

File: EmberLottery.sol

219: emit FeeSent(currentLotteryId, feeRecipient, fee);

223: emit WinnerSelected(currentLotteryId, winner, prize);

Link to code

[GAS-4] For Operations that will not overflow, you could use unchecked

Instances (18):

File: EmberLottery.sol

95: currentLotteryId++;

99: lottery.endTime = block.timestamp + _duration;

100: lottery.commitEndTime = lottery.endTime + _commitDuration;

126: uint256 cost = lottery.ticketPrice * _ticketCount;

134: ticketCount[currentLotteryId][msg.sender] += _ticketCount;

135: lottery.totalPot += cost;

139: SafeTransferLib.safeTransferETH(msg.sender, msg.value - cost);

179: blockhash(block.number - 1);

190: uint256 pastBlock = block.number - BLOCKHASH_ALLOWED_RANGE;

201: blockhash(block.number - 1);

214: uint256 fee = (lottery.totalPot * FEE_BPS) / MAX_BPS;

215: uint256 prize = lottery.totalPot - fee;

Link to code

[GAS-5] Use Custom Errors instead of Revert Strings to save Gas

Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit.

Instances (1):

File: EmberLottery.sol

112: require(block.timestamp < lottery.commitEndTime, "Commit period ended");

Link to code

[GAS-6] Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function.

Instances (2):

File: EmberLottery.sol

266: function setFeeRecipient(address _feeRecipient) external onlyOwner {

274: function emergencyWithdraw() external onlyOwner {

Link to code

[GAS-7] ++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1)

Pre-increments and pre-decrements are cheaper.

Instances (2):

File: EmberLottery.sol

95: currentLotteryId++;

130: for (uint256 i = 0; i < _ticketCount; i++) {

Link to code

[GAS-8] Using private rather than public for constants, saves gas

If needed, the values can be read from the verified contract source code.

Instances (3):

File: EmberLottery.sol

55: uint256 public constant FEE_BPS = 500; // 5% fee

56: uint256 public constant MAX_BPS = 10000;

57: uint256 public constant BLOCKHASH_ALLOWED_RANGE = 256; // Max blocks to use blockhash

Link to code

[GAS-9] Increments/decrements can be unchecked in for-loops

In Solidity 0.8+, there's a default overflow check on unsigned integers.

Instances (1):

File: EmberLottery.sol

130: for (uint256 i = 0; i < _ticketCount; i++) {

Link to code

[GAS-10] Use != 0 instead of > 0 for unsigned integer comparison

Instances (5):

File: EmberLottery.sol

90: if (currentLotteryId > 0) {

167: if (lottery.commitEndTime > 0 && block.timestamp >= lottery.commitEndTime) {

261: return lottery.endTime > 0 && block.timestamp < lottery.endTime && !lottery.ended;

276: if (lottery.participants.length > 0) revert NoParticipants();

279: if (balance > 0) {

Link to code

Non Critical Issues

IssueInstances
NC-1Use string.concat() or bytes.concat() instead of abi.encodePacked4
NC-2Control structures do not follow the Solidity Style Guide13
NC-3Consider disabling renounceOwnership()1
NC-4Unused error definition2
NC-5Functions should not be longer than 50 lines7
NC-6Missing Event for critical parameters change1
NC-7NatSpec is completely non-existent on functions that should have them1
NC-8Consider using named mappings4
NC-9Adding a return statement when the function defines a named return variable, is redundant1
NC-10Take advantage of Custom Error's return value property13
NC-11Contract does not follow the Solidity style guide's suggested layout ordering1
NC-12Use Underscores for Number Literals (add an underscore every 3 digits)1
NC-13Event is missing indexed fields5
NC-14Variables need not be initialized to zero1

[NC-1] Use string.concat() or bytes.concat() instead of abi.encodePacked

Instances (4):

File: EmberLottery.sol

172: bytes32 expectedCommit = keccak256(abi.encodePacked(_secret, msg.sender));

177: keccak256(abi.encodePacked(

192: keccak256(abi.encodePacked(

200: keccak256(abi.encodePacked(

Link to code

[NC-3] Consider disabling renounceOwnership()

Instances (1):

File: EmberLottery.sol

19: contract EmberLottery is Ownable, ReentrancyGuard {

Link to code

[NC-4] Unused error definition

Instances (2):

File: EmberLottery.sol

28: error TransferFailed();

31: error RevealTooEarly();

Link to code

[NC-5] Functions should not be longer than 50 lines

Instances (7):

File: EmberLottery.sol

110: function commit(uint256 _lotteryId, bytes32 _commitHash) external {

121: function buyTickets(uint256 _ticketCount) external payable nonReentrant {

149: function endLottery(bytes calldata _secret) external nonReentrant {

251: function getParticipants(uint256 _lotteryId) external view returns (address[] memory) {

255: function getTicketCount(uint256 _lotteryId, address _user) external view returns (uint256) {

259: function isLotteryActive() external view returns (bool) {

266: function setFeeRecipient(address _feeRecipient) external onlyOwner {

Link to code

[NC-6] Missing Event for critical parameters change

Instances (1):

File: EmberLottery.sol

266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
if (_feeRecipient == address(0)) revert ZeroAddress();
feeRecipient = _feeRecipient;

Link to code

[NC-7] NatSpec is completely non-existent on functions that should have them

Instances (1):

File: EmberLottery.sol

266: function setFeeRecipient(address _feeRecipient) external onlyOwner {

Link to code

[NC-8] Consider using named mappings

Instances (4):

File: EmberLottery.sol

50: mapping(address => bytes32) commits;

51: mapping(address => uint256) ticketCountPerUser;

63: mapping(uint256 => Lottery) public lotteries;

64: mapping(uint256 => mapping(address => uint256)) public ticketCount;

Link to code

[NC-9] Adding a return statement when the function defines a named return variable, is redundant

Instances (1):

File: EmberLottery.sol

228: function getLotteryInfo(uint256 _lotteryId)
external
view
returns (
uint256 ticketPrice,
uint256 endTime,
uint256 totalPot,
uint256 participantCount,
address winner,
bool ended
)
{
Lottery storage lottery = lotteries[_lotteryId];
return (

Link to code

[NC-13] Event is missing indexed fields

Instances (5):

File: EmberLottery.sol

34: event LotteryStarted(uint256 indexed lotteryId, uint256 ticketPrice, uint256 endTime);

35: event TicketPurchased(uint256 indexed lotteryId, address indexed buyer, uint256 ticketCount);

36: event WinnerSelected(uint256 indexed lotteryId, address indexed winner, uint256 prize);

37: event FeeSent(uint256 indexed lotteryId, address indexed feeRecipient, uint256 amount);

38: event Committed(uint256 indexed lotteryId, address indexed participant, bytes32 commit);

Link to code

[NC-14] Variables need not be initialized to zero

Instances (1):

File: EmberLottery.sol

130: for (uint256 i = 0; i < _ticketCount; i++) {

Link to code

Low Issues

IssueInstances
L-1Use a 2-step ownership transfer pattern1
L-2abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()1
L-3Loss of precision1
L-4Solidity version 0.8.20+ may not work on other chains due to PUSH01
L-5Use Ownable2Step.transferOwnership instead of Ownable.transferOwnership1
L-6Upgradeable contract not initialized1

[L-1] Use a 2-step ownership transfer pattern

Instances (1):

File: EmberLottery.sol

19: contract EmberLottery is Ownable, ReentrancyGuard {

Link to code

[L-2] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()

Instances (1):

File: EmberLottery.sol

172: bytes32 expectedCommit = keccak256(abi.encodePacked(_secret, msg.sender));

Link to code

[L-3] Loss of precision

Instances (1):

File: EmberLottery.sol

214: uint256 fee = (lottery.totalPot * FEE_BPS) / MAX_BPS;

Link to code

[L-4] Solidity version 0.8.20+ may not work on other chains due to PUSH0

Instances (1):

File: EmberLottery.sol

2: pragma solidity ^0.8.20;

Link to code

[L-5] Use Ownable2Step.transferOwnership instead of Ownable.transferOwnership

Instances (1):

File: EmberLottery.sol

4: import {Ownable} from "solady/auth/Ownable.sol";

Link to code

[L-6] Upgradeable contract not initialized

Instances (1):

File: EmberLottery.sol

69: _initializeOwner(msg.sender);

Link to code

Medium Issues

IssueInstances
M-1block.number means different things on different L2s4
M-2Centralization Risk for trusted owners4

[M-1] block.number means different things on different L2s

Instances (4):

File: EmberLottery.sol

179: blockhash(block.number - 1),

189: if (block.number > BLOCKHASH_ALLOWED_RANGE) {

190: uint256 pastBlock = block.number - BLOCKHASH_ALLOWED_RANGE;

201: blockhash(block.number - 1),

Link to code

[M-2] Centralization Risk for trusted owners

Impact:

Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.

Instances (4):

File: EmberLottery.sol

19: contract EmberLottery is Ownable, ReentrancyGuard {

85: ) external onlyOwner {

266: function setFeeRecipient(address _feeRecipient) external onlyOwner {

274: function emergencyWithdraw() external onlyOwner {

Link to code


🦞 Clawditor AI Summary

Overview

EmberLottery is a simple, gas-optimized lottery contract built with Solady. Users buy tickets with ETH, and a winner is selected to take the pot minus a 5% fee to stakers.

Security Assessment

✅ Strengths

  • Clean Solady implementation (gas-optimized)
  • Proper use of SafeTransferLib for ETH transfers
  • ReentrancyGuard on all state-modifying functions
  • Input validation on critical parameters
  • Emergency withdraw protection

⚠️ Findings

1. Predictable Randomness (Medium)

  • Uses blockhash(block.number - 1) + block.timestamp + participants.length
  • Miner/front-runner can manipulate blockhash and timestamp
  • Fix: Use commit-reveal scheme or Chainlink VRF for production

2. Blockhash Availability (Low)

  • blockhash() returns bytes(0) for blocks older than 256
  • If lottery runs >256 blocks without a winner, randomness breaks
  • Fix: Check block number and use alternative entropy if needed

3. Front-Running on buyTickets (Medium)

  • Users can see pending transactions and buy tickets at end
  • MEV bots could sandwich ticket purchases
  • Fix: Add commit-reveal for ticket purchases

4. Unbounded Array Storage (Medium)

  • participants.push(msg.sender) for each ticket
  • If a user buys 1000 tickets, 1000 storage writes
  • Fix: Use mapping for ticket counts, only push unique addresses

Risk Level: LOW-MODERATE

For a lottery with simple randomness and noted Chainlink VRF upgrade path, the risk is acceptable for testing. Not recommended for production without VRF integration.

Recommendations

PriorityIssueFix
HighPredictable randomnessImplement Chainlink VRF
MediumFront-runningAdd commit-reveal scheme
MediumStorage DoSUse mapping for ticket counts
LowBlockhash expiryAdd block number check

Audit performed by Clawditor AI | Report generated: 2026-01-29

  • Audit Metadata
  • 🔬 Analyzer Technical Report
  • Gas Optimizations
    • [GAS-1] a = a + b is more gas effective than a += b for state variables (excluding arrays and mappings)
    • [GAS-2] Use assembly to check for address(0)
    • [GAS-3] State variables should be cached in stack variables rather than re-reading them from storage
    • [GAS-4] For Operations that will not overflow, you could use unchecked
    • [GAS-5] Use Custom Errors instead of Revert Strings to save Gas
    • [GAS-6] Functions guaranteed to revert when called by normal users can be marked payable
    • [GAS-7] ++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1)
    • [GAS-8] Using private rather than public for constants, saves gas
    • [GAS-9] Increments/decrements can be unchecked in for-loops
    • [GAS-10] Use != 0 instead of > 0 for unsigned integer comparison
  • Non Critical Issues
    • [NC-1] Use string.concat() or bytes.concat() instead of abi.encodePacked
    • [NC-3] Consider disabling renounceOwnership()
    • [NC-4] Unused error definition
    • [NC-5] Functions should not be longer than 50 lines
    • [NC-6] Missing Event for critical parameters change
    • [NC-7] NatSpec is completely non-existent on functions that should have them
    • [NC-8] Consider using named mappings
    • [NC-9] Adding a return statement when the function defines a named return variable, is redundant
    • [NC-13] Event is missing indexed fields
    • [NC-14] Variables need not be initialized to zero
  • Low Issues
    • [L-1] Use a 2-step ownership transfer pattern
    • [L-2] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()
    • [L-3] Loss of precision
    • [L-4] Solidity version 0.8.20+ may not work on other chains due to PUSH0
    • [L-5] Use Ownable2Step.transferOwnership instead of Ownable.transferOwnership
    • [L-6] Upgradeable contract not initialized
  • Medium Issues
    • [M-1] block.number means different things on different L2s
    • [M-2] Centralization Risk for trusted owners
  • 🦞 Clawditor AI Summary