Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · November 2025

Protocol Review

for Footium

Guardian's review of Protocol Review for Footium, published November 2025. The report records 28 findings across 3 review rounds, including 1 high and 3 medium.

Published
Review window
November 11, 2025 to May 29, 2026
Rounds
Main Review, Remediation Review, Remediation Review 2
Language
Solidity
Chains
Arbitrum
Sector
Gaming and prediction, NFTs
  • 0 Critical
  • 1 High
  • 3 Medium
  • 12 Low
  • 12 Informational

13 resolved · 15 acknowledged

Scope

1 file in scope · 113 nSLOC
FilenSLOCLines
contracts/FootiumGeneralMinting.sol113203

Findings 28

Main Review

21 findings · November 11 to 12, 2025
  1. H-01 High Payment Amount Not Multiplied By Mint Quantity Logical Error Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    The mint() function in FootiumGeneralMinting accepts a value parameter that represents the payment amount and an amountparameter representing the number of NFTs to mint. However, the payment validation logic does not multiply value by amount, allowing users to mint multiple NFTs while paying only for a single token.

    User ends up severely underpaying since users pay a flat value regardless of how many NFTs they mint. The technical documentation in FootiumGeneralMinting.sol states:

    // Setup: Anyone can mint up to 5 NFTs for 0.05 ETH each
    // In merkle tree: [mintId, false, 0, 5, address(0), 0.05 ether]
    

    However, the implementation contradicts this by treating value as a total payment rather than a per-NFT price.

    Recommendation

    Multiply the configured price by amount

  2. M-01 Medium Bypass Of Non-Holder Mints Logical Error Acknowledged
    Location
    contracts/FootiumGeneralMinting.sol:150
    Round
    Main Review

    Description

    “Non-holder” mints (excludeParentNftOwners == true, parentNftId == 0) only check _parentNft.balanceOf(receiver). A holder can still call mint and forward NFTs to a fresh wallet, bypassing the intent to prevent existing holders from minting.

    Recommendation

    If the goal is to block holders from participating at all, validate _parentNft.balanceOf(msg.sender) == 0 (or force receiver == msg.sender) when exclusion mode is used. Otherwise clarify in docs that the current check only prevents delivery to holder wallets, not holder participation.

  3. M-02 Medium Multi-minting DoS'd With Token Id Provided Logical Error Resolved
    Location
    FootiumGeneralMinting.sol: 162
    Round
    Main Review

    Description

    Multi-minting a fixed tokenId reuses the same identifier inside the mint loop. Unless the underlying IFootiumMintable ignores the parameter (auto-increment mode), any call with amount > 1 and an explicit tokenId will revert on the second iteration, halting the entire mint process.

    Recommendation

    Ensure to derive unique IDs per iteration or require that amount == 1 when tokenId != 0.

  4. M-03 Medium Access Control Not Tied To Ownership OOS Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    During initialize the contract grants DEFAULT_ADMIN_ROLE to msg.sender, but transferOwnership does not reassign or revoke that admin role. Once the owner hands the contract to a multisig or new team, the previous owner still has full admin authority (can grant or revoke MINTER_ROLE, change other admins, etc.), while the new owner cannot manage minters because DEFAULT_ADMIN_ROLE never moved.

    Recommendation

    Update ownership transfers (or add a setter) so AccessControl admins mirror the current owner, and revoke the old admin when ownership changes.

  5. L-01 Low Implementation Can Be Initialized Directly Warning Resolved
    Location
    Global
    Round
    Main Review

    Description

    None of the upgradeable contracts disable their initializers on the implementation contract. An attacker can call initialize on the implementation directly and become the implementation owner/admin. While this does not affect proxies directly, it enables phishing and operator confusion, and can be dangerous if the implementation ever receives ETH or is otherwise interacted with by mistake.

    Recommendation

    Add a constructor that calls _disableInitializers() in each upgradeable contract to lock the implementation.

  6. L-02 Low No Validation Against Zero-Amount Mints Validation Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    The mint() function in FootiumGeneralMinting does not validate that amount > 0 before processing payment. A user can successfully call the function with amount = 0 while providing a non-zero value, resulting in payment being transferred to the payment receiver without any NFTs being minted to the user. The payment logic executes regardless of the amount, while the minting loop only executes if amount > 0. This creates a scenario where users can lose funds without receiving any tokens in return.

    Recommendation

    Add validation to require non-zero amount.

  7. L-03 Low Weird Tokens Not Supported Warning Resolved
    Location
    Global
    Round
    Main Review

    Description

    The FootiumPrizeDistributor and FootiumGeneralMinting contracts assume that ERC20 token transfers move the exact amount specified. This assumption breaks for tokens with non-standard transfer mechanics.

    Recommendation

    Document that only standard ERC20 tokens are supported

  8. L-04 Low Cross-Chain Replay Attacks Warning Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    The merkle leaf construction in FootiumGeneralMinting, FootiumAcademy, and FootiumPrizeDistributor contracts does not include any chain-specific identifier such as block.chainid or a deployment salt. The leaf hashes only contain mint parameters like addresses, amounts, and IDs. If the protocol deploys the same merkle root across multiple chains for operational convenience, users can replay their valid merkle proofs on every chain where the contract is deployed.

    Recommendation

    Include a chain id for the proof if the plan is to go cross-chain.

  9. L-05 Low Storage Gaps Missing Warning Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    FootiumGeneralMinting is an upgradeable contract (PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable) but does not reserve storage slots for future variables. Without a _gap array, any future version that adds state variables will shift inherited storage layout and can corrupt parent storage.

    Recommendation

    Consider appending a gap to storage.

  10. L-06 Low Per-user Limits Bypassed Logical Error Acknowledged
    Location
    FootiumGeneralMinting.sol: 258
    Round
    Main Review

    Description

    Per-user limits are keyed to receiver, not to the caller: _checkMintLimit updates _mintCounts[mintId][receiver] and the Merkle leaf omits receiver whenever isAddressReserved is false. A single account can therefore reuse the same proof indefinitely simply by changing the receiver argument to fresh addresses they control, defeating “per user” or “per proof” caps while still paying once per call. An arbitrary user can also intentionally mint to a victim’s address, consuming that victim’s entire mint allowance (although there may be little incentive to do this as the user would pay for the victim’s mint).

    Recommendation

    Consider if this is expected behavior for the protocol. If not, either bind the receiver into the proof even for public mints or track limits by msg.sender.

  11. L-07 Low ERC20 Tokens That Do Not Return Boolean Fail Warning Resolved
    Location
    FootiumGeneralMinting.sol: 282
    Round
    Main Review

    Description

    Function _handlePayment() assumes all ERC20 tokens return a boolean value from transferFrom(). However, some widely-used tokens like USDT (on Ethereum mainnet) do not return any value from their transfer() and transferFrom() functions. When Solidity attempts to decode an empty return value as a bool, the transaction will revert with an ABI decoding error.

    Recommendation

    Consider using SafeERC20 or set the _paymentToken carefully.

  12. L-08 Low Missing ERC-20 Withdraw Function Best Practices Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    The contract currently supports receiving both native ETH and ERC-20 tokens. However, only native ETH can be recovered via the existing withdraw function. If ERC-20 tokens are accidentally transferred to the contract (for example, by users sending tokens directly), those tokens remain permanently stuck since there is no recovery mechanism for them.

    Recommendation

    Add an emergency withdrawal function for arbitrary ERC-20 tokens, restricted to owner role.

  13. L-09 Low Merkle Root Update Can Brick User Claims OOS Acknowledged
    Location
    FootiumPrizeDistributor
    Round
    Main Review

    Description

    The FootiumPrizeDistributor contract allows the owner to update the merkle root at any time via setERC20MerkleRoot() and setETHMerkleRoot(). The claim functions calculate the claimable amount as: uint256 value = _amount - totalERC20Claimed[_token][_to];

    If the owner updates the merkle root with a cumulative _amount that is lower than what the user has already claimed (totalERC20Claimed[_token][_to]), the subtraction will underflow and revert, blocking user claims.

    Recommendation

    Add validation to ensure merkle root updates don't reduce cumulative amounts below already-claimed totals.

  14. L-10 Low Player ID Uniqueness Check Is Global OOS Acknowledged
    Location
    FootiumAcademy.sol
    Round
    Main Review

    Description

    The FootiumAcademy contract tracks minted players using a mapping keyed only by playerId: mapping(string => bool) private _mintedPlayers;

    The contract checks require(_mintedPlayers[playerId] == false, "Player already minted"); so once the id is minted, it cannot be minted again.

    However, the merkle proof verification includes both clubId and playerId: bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(clubId, playerId, msg.value))));

    The off-chain merkle tree can legitimately contain multiple entries with the same playerId but different clubId values. However, once any club mints a player with a given playerId, no other club can mint a player with that same ID, even if they have a valid merkle proof.

    Recommendation

    Change the _mintedPlayers mapping to be keyed by both clubId and playerId.

  15. L-11 Low Player IDs Are Case-Sensitive OOS Acknowledged
    Location
    FootiumAcademy.sol
    Round
    Main Review

    Description

    The FootiumAcademy contract uses string type for player IDs without any normalization or case-insensitive comparison. The _mintedPlayers mapping stores player IDs exactly as provided, meaning "Player-123", "player-123", and "PLAYER-123" are treated as three completely different players. The merkle proof verification encodes the player ID string as-is, so case variations produce different merkle leaves and are considered valid distinct entries. If the off-chain merkle tree generation system doesn't enforce consistent casing, the same logical player can be minted multiple times with different case variations.

    Recommendation

    Ensure the off-chain system normalizes player ID's and does not multiple entries for the same player.

  16. L-12 Low Contracts Unable To Receive ETH Price OOS Acknowledged
    Location
    FootiumPrizeDistributor.sol: 185
    Round
    Main Review

    Description

    The claimETHPrize function transfers ETH using a low-level call without providing an alternative claim mechanism. The function enforces that only the prize recipient can claim (_to != msg.sender check), and if the recipient is a contract address without a receive() or payable fallback() function, the ETH transfer will always fail, locking the funds for the recipient.

    Recommendation

    Clearly document this behavior.

  17. I-01 Informational Incorrect NatSpec For parentNftId Documentation Resolved
    Location
    FootiumGeneralMinting.soll
    Round
    Main Review

    Description

    The NatSpec for mint states parentNftId … (0 or type(uint256).max to skip ownership check). However, the implementation only treats parentNftId == 0 as the skip condition. If the backend follows the documented type(uint256).max sentinel, calls will reach the ownerOf(parentNftId) branch and revert, permanently disabling those mint configurations.

    Recommendation

    Update the NatSpec or adjust the ownership check code in mint to be synchronized.

  18. I-02 Informational Missing NatSpec For excludeParentNftOwners Documentation Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    In the mint function, the excludeParentNftOwners input variable does not have a NatSpec, unlike the other variables.

    Recommendation

    Add a NatSpec @param excludeParentNftOwners entry that clearly explains the two behaviors (require ownership vs. exclude owners).

  19. I-03 Informational Missing Zero-Address Validation Warning Resolved
    Location
    FootiumGeneralMinting.sol: 52-68
    Round
    Main Review

    Description

    The initialize() function in FootiumGeneralMinting does not validate that the provided addresses are non-zero before assigning them to critical contract state variables.

    Recommendation

    Add zero-address validation for critical parameters in the initialize() function

  20. I-04 Informational Parent NFT Ownership Misses Combinations Documentation Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    The _checkParentNftOwnership() function only validates two specific combinations of parentNftId and excludeParentNftOwners parameters, leaving other combinations unchecked:

    (1) parentNftId != 0 AND excludeParentNftOwners == true

    (2) parentNftId == 0 AND excludeParentNftOwners == false

    The intent of these parameter combinations are ambiguous. For example, if excludeParentNftOwners is true, the contract can enforce that the parent NFT balance of the receiver is 0, but that would be contradictory with the defined parentNftId having to be owned by a user.

    Recommendation

    Clearly document these no-op combinations.

  21. I-05 Informational Mint Distribution Cannot Be Enforced Warning Resolved
    Location
    FootiumGeneralMinting.sol
    Round
    Main Review

    Description

    The FootiumGeneralMinting contract cannot enforce transaction-level pacing or batch-size restrictions as the amount parameter is not included in the merkle proof. A user can mint their entire allocation in a single transaction (amount = limit) or split it across multiple calls.

    Recommendation

    Consider if this is okay for the purposes for the general minting contract and clearly document this behavior.

Remediation Review

4 findings · November 15 to 16, 2025
  1. I-01 Informational Not All Validations For Zero Address Added Best Practices Acknowledged
    Location
    FootiumGeneralMinting.sol: 61
    Round
    Remediation Review

    Description

    Parameters parentNft and paymentToken are not checked against the zero address unlike mintableNft and paymentReceiverAddress.

    Recommendation

    Consider adding zero address checks for those variables as well.

  2. I-02 Informational Missing Event On Native ETH Withdrawal Best Practices Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review

    Description

    The recoverERC20 function emits an ERC20Recovered event, but the native ETH withdraw path does not emit any event.

    Recommendation

    Consider emitting a dedicated event for native ETH withdrawals to ensure parity with ERC20 recovery and improving observability for all recovery actions.

  3. I-03 Informational NatSpec Misalignment For mint Best Practices Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review

    Description

    The NatSpec on mint: excludeParentNftOwners currently describes only one way to bypass the parent-NFT ownership check, but _checkParentNftOwnership actually supports multiple skip conditions. This discrepancy can mislead integrators into assuming ownership is enforced in cases where it is not.

    Likewise, the value parameter is documented only as “The payment amount,” even though the total cost is computed as value * amount, which may cause confusion.

    Recommendation

    Simplify the NatSpec so it does not state the specific bypass combinations. Instead, keep the high-level description in mint general, and rely on the detailed four-case commentary already present inside _checkParentNftOwnership to define the exact behavior.

    Also clarify in the NatSpec that value represents the per-NFT price, not the total payment.

  4. I-04 Informational Multiple Leaves For Same User/Token ID Warning Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review

    Description

    It is possible for the same receiver to have multiple leaves and for the same tokenId but with different values. Consequently, a user can claim a specific NFT but for a cheaper price than encoded in another leaf. Furthermore, with multiple leaves and a specific tokenId, only the first user to mint will be able to receive that specific token.

    Recommendation

    Clearly document this behavior and ensure the Merkle tree is appropriately constructed to spec.

Remediation Review 2

3 findings · May 29, 2026
  1. I-01 Informational Global mints depend on leaf limits Warning Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review 2

    Description

    In global mint-count mode, uniqueness is only enforced if the Merkle leaf uses limit = 1. If limit = 0, the mint is unlimited and untracked; if limit > 1, the same global mintId can be minted multiple times.

    Recommendation

    Enforce limit = 1 for global mints (backend), and add contract-level protection to reject limit = 0 or limit > 1 when useGlobalMintCounts is enabled.

  2. I-02 Informational Global count flag should be immutable Best Practices Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review 2

    Description

    useGlobalMintCounts is intended to be a deployment-time mode flag, but it is currently stored as a normal storage variable.

    Recommendation

    Make useGlobalMintCounts immutable.

  3. I-03 Informational Payment receiver can be set to zero Validation Acknowledged
    Location
    FootiumGeneralMinting.sol
    Round
    Remediation Review 2

    Description

    setPaymentReceiverAddress does not reject address(0), even though the constructor rejects a zero payment receiver. If the owner accidentally sets the payment receiver to zero, future mints will forward payments to address(0) .

    Recommendation

    Add the same zero-address validation to setPaymentReceiverAddress.

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote