You are on page 1of 60

Smart Contract Security Report

for

Keepers

Prepared By: Jonathan S


Ginger Security
June 12, 2023

1/9 Keepers Smart Contract Security Report | Ginger Security


Table of Contents
General Information 3

Contact 3

About Revelator 4

About Ginger Security 4

Methodology 5

Severity Definition 5

Disclaimers 6

Executive Summary 7

Scope 8

Key Findings 9

Findings Details 10

2/9 Keepers Smart Contract Security Report | Ginger Security


General Information

Title Smart Contract Security Report

Client Keepers

Website https://keepers.xyz/

Platform EVM, Solidity

Product Keepers Smart Contracts

Author Jonathan S

Auditors Jonathan S, Daniel K, Carlos F, Kristian A,


Priyam S

Audit Start 2023.06.01

Audit End 2023.06.12

Classification Public

Contact
For more information please contact Ginger Security Inc.: hello@gingersec.xyz

3/9 Keepers Smart Contract Security Report | Ginger Security


About Keepers
Keepers is a unique and high-value NFT collection that pushes the boundaries of on-chain
possibilities. It explores the potential of on-chain capabilities. It combines efficient metadata
storage, user customization, fair token allocation, transparent terms of use, metaverse room
naming, and token gating authority, providing an engaging experience for NFT enthusiasts.

Keepers store NFT metadata on the blockchain, offers an interactive web-based configurator for
personalized avatars, and uses a commit/reveal mechanism together with Randao for random
token ID allocation. The collection also includes on-chain terms of use, allows for the naming of
future metaverse rooms, and provides contract administrators with the ability to revoke token
holders' licenses. By minting/purchasing a ticket, users gain access to the configurator to
customize their avatars, selecting attributes with incremental pricing and limited availability.

About Ginger Security


At Ginger Security, we are dedicated to providing the highest level of protection for your
blockchain assets and applications. Our team of expert security professionals, including former
blackhat hackers, have the knowledge and experience to ensure the integrity and safety of your
smart contracts and other web3 assets.

Our unique incentivized security model means that you only pay for our services if we are able to
successfully hack your app. In the unlikely event that your app is hacked after our audit, our
revenue is slashed in partnership with hats.finance, provable on-chain. This ensures that we
have a vested interest in providing the most effective security services possible.

In addition to smart contract auditing, we offer full-stack protection for your blockchain projects.
This includes expertise in protocol design, tokenomics, web2, and anti-phishing. Contact us
today to learn more about how Ginger Security can help protect your assets and reputation on
the blockchain.

4/9 Keepers Smart Contract Security Report | Ginger Security


Methodology
We take a holistic approach to smart contract auditing and use a combination of manual
analysis and cutting-edge tools to thoroughly test your contracts and provide you with a
comprehensive security assessment.

Our team of experts begins by conducting a manual review of your contract, using our extensive
knowledge and experience to identify potential vulnerabilities or weaknesses. We also use
advanced static analysis tools to automatically scan your code for common security issues.

Once we have identified potential risks, we conduct dynamic analysis to test your contract in a
safe and controlled environment. This involves executing the contract with a variety of input
scenarios and monitoring its behavior to uncover hidden vulnerabilities. We also use symbolic
execution and formal verification techniques to rigorously test the behavior of your contract and
ensure its correctness.

Throughout the auditing process, we provide regular updates and progress reports to keep you
informed of our findings and recommendations. Our goal is to provide you with a detailed and
actionable security assessment that can help you improve the security and reliability of your
contract

Severity Definition
In Ginger Security, we use a system of severity levels to classify the vulnerabilities that we find
during our security audits. Our severity levels include High, Medium, and Low, and are
determined based on the potential impact of the vulnerability on the security and functionality of
the contract. High-severity vulnerabilities pose a significant risk to the contract and should be
addressed as soon as possible. Medium-severity vulnerabilities are less severe, but still require
attention and should be addressed in a timely manner. Low-severity vulnerabilities are the least
severe, but may still need to be addressed depending on the specific circumstances of the
contract.

5/9 Keepers Smart Contract Security Report | Ginger Security


Disclaimers
Please note that a security audit is not a guarantee of 100% safety. It is important for users and
clients to always do their own due diligence and not rely solely on the results of an audit. The
auditing process is designed to identify potential vulnerabilities and weaknesses, but it is not
foolproof. There may be risks that are not uncovered during an audit, and it is the responsibility
of the user or client to take appropriate measures to protect their assets and mitigate those
risks. Ginger Security cannot be held liable for any losses or damages resulting from the use of
our services. We strongly recommend that users and clients carefully evaluate all risks and take
appropriate measures to protect their assets.

6/9 Keepers Smart Contract Security Report | Ginger Security


Executive Summary
In May 2023, Ginger Security was contracted by the Keepers team to conduct a security
assessment of their Keepers Smart Contracts. The assessment involved reviewing code
functionality, performing automated checks, and conducting a manual smart contract audit.

During the audit, our team identified several potential vulnerabilities that should be addressed to
ensure the security and reliability of the Keepers Smart Contracts. We have provided detailed
recommendations to the Keepers team to help them enhance the security of their contracts.

Although Ginger Security found vulnerabilities during the audit, we have confidence in the overall
security of the Keepers Smart Contracts. We remain committed to supporting the Keepers team
in their ongoing efforts to maintain the highest level of security for their products. Our goal is to
provide exceptional security services and support to our clients.

Issues that were found during the audit:

High Severity - 5 Vulnerabilities

Medium Severity - 15 Vulnerabilities

Low Severity - 5 Vulnerabilities

7/9 Keepers Smart Contract Security Report | Ginger Security


Scope
The scope of the project is the following smart contracts:

Filename nSLOC (Solidity Lines of Code)

contracts/facets/AvatarAssignment/Keepers 232
AvatarAssignment.sol

contracts/facets/KeepersERC721/ERC721r.s 142
ol

contracts/facets/KeepersERC721/KeepersER 94
C721Metadata.sol

contracts/facets/RoomNaming/RoomNamin 92
gFacet.sol

contracts/library/ConfigLib.sol 63

contracts/facets/KeepersERC721/KeepersER 57
C721.sol

contracts/library/PseudoRandomLib.sol 41

contracts/facets/Terms/TermsFacet.sol 40

contracts/KeepersDiamond.sol 39

contracts/facets/License/LicenseFacet.sol 35

contracts/facets/KeepersAdmin/KeepersAd 35
minFacet.sol

contracts/facets/KeepersERC721/ERC721Ba 34
se.sol

contracts/facets/AvatarAssignment/Keepers 25
AvatarAssignmentStorage.sol

contracts/facets/KeepersERC721/KeepersER 25
C721Storage.sol

contracts/facets/KeepersERC721/KeepersMi 24
ntWindowModifiers.sol

8/9 Keepers Smart Contract Security Report | Ginger Security


contracts/facets/RoomNaming/RoomNamin 17
gStorage.sol

contracts/facets/Terms/TermsStorage.sol 16

contracts/facets/License/LicenseStorage.sol 13

9/9 Keepers Smart Contract Security Report | Ginger Security


Key Findings
The smart contracts are well-designed and engineered, but the implementation can be improved
by addressing the following issues:

ID Severity Title Status

H-01 High Users May Lose ETH Due to Wrong Supply


Calculation in the Commit Phase

H-02 High Immutability Cannot be Achieved for


KeepersDiamond

H-03 High Potential for User Funds to be Stuck in ERC721r


Contract Due to Expired Commitments

H-04 High Insufficient Access Control For Minting

H-05 High There is no Refund Function for Commits That


Can’t be Fulfilled

H-06 High Tickets Can’t be Revealed and Converted to


Avatars When The Mint Window is Closed

H-07 High Inaccurate Rarity Distribution in Randomly


Assigned Avatar Configurations

H-08 High A Malicious User Can Mint Tokens With


Deterministic IDs

H-09 High Protocol Design: There is No Incentive To Pay for


Traits When You Can Get Them For Free Once
Minting Window is Closed

M-01 Medium Inconsistent Role Identifier LICENSE_OPERATOR


between KeepersDiamond.sol and
LicenseFacet.sol

M-02 Medium Inconsistency in Maximum Tickets Allowed Per


Wallet

M-03 Medium Risk of Lost Tokens due to Use of _mint Instead


of _safeMint

M-04 Medium Fixed Size Array Limits Upgradability of

10/9 Keepers Smart Contract Security Report | Ginger Security


ID Severity Title Status

RoomNamingFacet in Diamond Proxy Pattern

M-05 Medium Multiple Methods are Marked as Payable but Do


Not Receive Any Ether

M-06 Medium Incorrect Base64 Data URL Scheme Used in


_tokenURI of KeepersERC721Metadata

M-07 Medium Usage of transfer Instead of call in


KeepersAdminFacet

M-08 Medium The Diamond Contract Deployer is a Single point


of failure without any Mitigation

M-09 Medium The Requirement That The Call is From an EOA


Might Not Hold True In The Future

M-10 Medium State Can Be Changed When The System is


Paused

M-11 Medium Sale Settings Are Mutable During The Sale


Phase

M-12 Medium Lack of Proper Unit Testing and Documentation

M-13 Medium No Proper Validation for minCommitmentBlocks


and maxCommitmentBlocks

M-14 Medium Implementated Code Does Not Align with the


Provided Documentation

M-15 Medium Potential Denial of Service Due to The 72 Hours


Window for Minting Not Being Initialized

M-16 Medium Wrong IMG Base URL In _tokenURI function

M-17 Medium Broken IMGs URI for Special Tickets Converted


to Avatars

M-18 Medium Unexpected Revert Due to a Chance of ID


Collission

M-19 Medium Reverse Bitmaps Causes a User’s Random Trait


Selection to Get Harmed

11/9 Keepers Smart Contract Security Report | Ginger Security


ID Severity Title Status

L-01 Low No Checks on Start and Completion Time in


Mint Window

L-02 Low Missing zero-address Check

L-03 Low Open TODOs

L-04 Low Potential ReEntrancy Attacks Due to not


following the CEI Pattern

L-05 Low Inconsistency in Solidity Compiler Versions and


Floating Pragrma

L-06 Low Missing Events in State Changing and Critical


Functions

L-07 Low Using block.difficulty to Retrieve


Pseudo-Random Numbers is Considered a
Deprecated Approach

L-08 Low Remove unused functions such as


_mintAtIndex

L-09 Low Should Allow Approved Accounts to Convert


Tickets to Avatars

L-10 Low No Inidication That


bulkAssignRandomAvatarConfigs Failed to
Assign a Specific TokenId

I-01 Informational Gas Optimization: Struct Variable Tight Packing


in KeepersERC721Storage

I-02 Informational Inconsistent Naming of Storage Variables

I-03 Informational Inconsistent Naming of the KeepersERC721


Facet Contract

I-04 Informational Redefinition of Multiple Constants

I-05 Informational Usage of Multiple Magic Numbers and Strings

I-06 Informational Unused Base-Contract AccessControlInternal


in KeepersAdminFacet

12/9 Keepers Smart Contract Security Report | Ginger Security


ID Severity Title Status

I-07 Informational Remove Unused Imports

I-08 Informational Missed Opportunity for Gas Saving When


Burning Tokens Due to _beforeTokenTransfer
Override in KeepersERC721

I-09 Informational Diamond Facet Upgrade

I-10 Informational Consider Using Always Explicitly uint256


Instead of uint

I-11 Informational Gas Optimization: It is Not Necessary to


Instantiate uint256 Variables to Zero

I-12 Informational Gas Optimization: Use Named Return Variables


in a Function Returns to Save Gas

I-13 Informational Gas Optimization: Use !=0 Instead of > 0 for


uint Variables

I-14 Informational Extract Array’s Length to Local Variable Instead


Iterating on While/For Loops

I-15 Informational Gas Optimization: Use “unchecked” When


Incrementing and Decrementing uints

I-16 Informational Code Redability: Move Bitwise Operations to


Functions

I-17 Informational Inconsistency With uint size data types

I-18 Informational Change The Order of Functionalities to Save Gas

13/9 Keepers Smart Contract Security Report | Ginger Security


Keepers Findings Details
High
[H-01] Users May Lose ETH Due to Wrong Supply Calculation in the Commit Phase
Affected Functions
Description
Mitigation
[H-02] Immutability Cannot be Achieved for KeepersDiamond
Affected Functions
Description
Mitigation
[H-03] Potential for User Funds to be Stuck in ERC721r Contract Due to Expired Commitments
Affected Functions
Description
Mitigation
[H-04] Insufficient Access Control For Minting
Affected Functions
Description
Mitigation
[H-05] There is no Refund Function for Commits That Can’t be Fulfilled
Affected Contracts
Description
Mitigation
[H-06] Tickets Can’t be Revealed and Converted to Avatars When The Mint Window is Closed
Affected Functions
Description
Mitigation
[H-07] Inaccurate Rarity Distribution in Randomly Assigned Avatar Configurations
Affected Contract
Description
Mitigation
[H-08] A Malicious User Can Mint Tokens With Deterministic IDs
Affected Contract
Description
Mitigation
[H-09] Protocol Design: There is No Incentive To Pay for Traits When You Can Get Them For Free Once Minting Window is Closed
Description
Mitigation
Medium
[M-01] Inconsistent Role Identifier LICENSE_OPERATOR between KeepersDiamond.sol and LicenseFacet.sol

Affected Functions
Description
Mitigation
[M-02] Inconsistency in Maximum Tickets Allowed Per Wallet
Affected Functions
Description
Mitigation
[M-03] Risk of Lost Tokens due to Use of  _mint  Instead of  _safeMint
Affected Functions
Description
Mitigation
[M-04] Fixed Size Array Limits Upgradability of  RoomNamingFacet  in Diamond Proxy Pattern
Affected Functions

Keepers Findings Details 1


Description
Mitigation
[M-05] Multiple Methods are Marked as  Payable  but Do Not Receive Any Ether
Affected Functions
Description
Mitigation
[M-06] Incorrect Base64 Data URL Scheme Used in  _tokenURI  of  KeepersERC721Metadata
Affected Functions
Description
Mitigation
[M-07] Usage of transfer Instead of call in KeepersAdminFacet
Affected Functions
Description
Mitigation
[M-08] The Diamond Contract Deployer is a Single point of failure without any Mitigation
Description
Mitigation
[M-09] The Requirement That The Call is From an EOA Might Not Hold True In The Future
Description
Mitigation
[M-10] State Can Be Changed When The System is Paused
Affected Functions
Description
Mitigation
[M-11] Sale Settings Are Mutable During The Sale Phase
Affected Functions
Description
Mitigation
[M-12] Lack of Proper Unit Testing and Documentation
Description
Mitigation
[M-13] No Proper Validation for minCommitmentBlocks and maxCommitmentBlocks

Affected Functions
Description
Mitigation
[M-14] Implementated Code Does Not Align with the Provided Documentation
Affected Functions
Description
Mitigation
[M-15] Potential Denial of Service Due to The 72 Hours Window for Minting Not Being Initialized
Affected Functions
Description
Mitigation
[M-16] Wrong IMG Base URL In _tokenURI function
Affected Functions
Description
Mitigation
[M-17] Broken IMGs URI for Special Tickets Converted to Avatars
Affected Functions
Description
Mitigation
[M-18] Unexpected Revert Due to a Chance of ID Collission
Affected Functions
Description
Mitigation

Keepers Findings Details 2


[M-19] Reverse Bitmaps Causes a User’s Random Trait Selection to Get Harmed
Affected Functions
Description
Mitigation
Low
[L-01] No Checks on Start and Completion Time in Mint Window
Affected Functions
Description
Mitigation
[L-02] Missing zero-address Check
Affected Functions
Description
Mitigation
[L-03] Open TODOs
Affected Functions
Description
Mitigation
[L-04] Potential ReEntrancy Attacks Due to not following the CEI Pattern
Affected Functions
Description
Mitigation
[L-05] Inconsistency in Solidity Compiler Versions and Floating Pragrma
Affected Functions
Description
Mitigation
[L-06] Missing Events in State Changing and Critical Functions
Affected Functions
Description
Mitigation
[L-07] Using block.difficulty to Retrieve Pseudo-Random Numbers is Considered a Deprecated Approach
Affected Functions
Description
Mitigation
[L-08] Remove unused functions such as _mintAtIndex

Affected Functions
Description
Mitigation
[L-09] Should Allow Approved Accounts to Convert Tickets to Avatars
Affected Functions
Description
Mitigation
[L-10] No Inidication That bulkAssignRandomAvatarConfigs Failed to Assign a Specific TokenId
Affected Functions
Description
Mitigation
Informational
[I-01] Gas Optimization: Struct Variable Tight Packing in KeepersERC721Storage
Affected Functions
Affected Functions
Description
Mitigation
[I-02] Inconsistent Naming of Storage Variables
Affected Functions
Description
Mitigation

Keepers Findings Details 3


[I-03] Inconsistent Naming of the KeepersERC721 Facet Contract
Affected Components
Description
Mitigation
[I-04] Redefinition of Multiple Constants
Affected Components
Description
Mitigation
[I-05] Usage of Multiple Magic Numbers and Strings
Affected Functions
Description
Mitigation
[I-06] Unused Base-Contract AccessControlInternal in KeepersAdminFacet

Affected Functions
Description
[I-07] Remove Unused Imports
Description
[I-08] Missed Opportunity for Gas Saving When Burning Tokens Due to  _beforeTokenTransfer  Override in  KeepersERC721
Affected Functions
Description
[I-09] Diamond Facet Upgrade
Affected Functions
Description
[I-10] Consider Using Always Explicitly uint256 Instead of uint

Affected Contracts
Description
[I-11] Gas Optimization: It is Not Necessary to Instantiate uint256 Variables to Zero
Affected variables
Description
[I-12] Gas Optimization: Use Named Return Variables in a Function Returns to Save Gas
Affected functions
Description
[I-13] Gas Optimization: Use !=0 Instead of > 0 for uint Variables
Affected code
Description
[I-14] Extract Array’s Length to Local Variable Instead Iterating on While/For Loops
Affected code
Description
[I-15] Gas Optimization: Use “unchecked” When Incrementing and Decrementing uints
Affected code
Description
[I-16] Code Redability: Move Bitwise Operations to Functions
Affected Contact
Description
[I-17] Inconsistency With uint size data types
Affected Contact
Description
[I-18] Change The Order of Functionalities to Save Gas
Affected Contact
Description

High

Keepers Findings Details 4


[H-01] Users May Lose ETH Due to Wrong Supply Calculation in the Commit
Phase
Affected Functions
ERC721r

commit

Description
The process of purchasing an NFT occurs in two distinct phases known as "commit" and "reveal." During the commit phase,
the user places an order and submits payment for the desired NFT tickets. In the subsequent reveal phase, the user is
granted ownership of the NFTs.
In both the commit and reveal functions, a validation check is implemented to prevent exceeding the maximum supply limit,
which is set at 20,000 tickets. This check ensures that the number of NFTs being purchased does not surpass the
predefined limit.
commit function validation:

// ERC721r.sol

if (currentSupply() + numNFTs > MAX_TICKETS) {


revert InvalidTicketCount(MAX_TICKETS, currentSupply(), numNFTs);
}

reveal function validation (inside the internal _validateCommitForReveal function):

//ERC721r.sol

if (currentSupply() + pendingCommit.numNfts > MAX_TICKETS) {


revert InvalidTicketCount(MAX_TICKETS, currentSupply(), pendingCommit.numNfts);
}

The validation check is only relevant during the reveal function as NFT minting occurs exclusively after its execution. (either
inside the internal _mintRandomTokenId or _mintAtIndex functions):

// ERC721r.sol

--updatedNumAvailableTokens;
l._numAvailableTokens = updatedNumAvailableTokens;

// ERC721r.sol

--l._numAvailableTokens;

Hence, during the commit phase, the currentSupply , which relies on _numAvailableTokens , remains unchanged.
Consider the following scenario:

1. There are 19,990 minted tickets (currentSupply).

2. Three accounts attempt to mint 8 tickets each.

3. For all three accounts, the commit transaction will not be reverted. Their commits will be registered, and the
corresponding ETH will be transferred to the contract.

Keepers Findings Details 5


4. However, only the first user who calls the reveal function will receive their tickets. The transactions of the other two
users will be reverted due to the validation check implemented in the _validateCommitForReveal function.

5. As a result, both users will lose 0.4 ETH each (0.05ETH * 8 tickets) without receiving their NFTs.

Mitigation
Consider addind an additional state variable to maintain a record of all the "pending commit" tickets. Furthermore, modify the
validation process in the commit function to incorporate the pending commit tickets in the calculation of the "not yet minted"
currentSupply. Here is an example of the new commit function validation:

// ERC721r.sol

if (currentSupply() + pendingTickets + numNFTs > MAX_TICKETS) {


revert InvalidTicketCount(MAX_TICKETS, currentSupply(), numNFTs);
}

[H-02] Immutability Cannot be Achieved for KeepersDiamond


Affected Functions
KeepersDiamond

diamondCut

Description
The documentation specifies that  KeepersDiamond  should renounce its upgradability at some stage:

At some point, contract upgradeability is renounced

Nevertheless, removing the  diamondCut  function by eliminating a Facet from the Diamond isn’t feasible. This is because
the  diamondCut  function isn’t added as a facet, rather it’s inherited from  DiamondWritable  via  SolidStateDiamond .
The implementation of  SolidStateDiamond  disallows the removal of inherited function selectors. Even if it was possible to
remove the function selector, the  diamondCut  function could still be invoked through the Contract’s own function, as there is
no proxying.

The Proof of Concept (POC) below demonstrates that the attempted call will revert:

// Add to test/diamond/Diamond.ts

it("POC H02 - diamondCut can't be replaced or removed because it is immutable", async function () {
const FACET_CUT_ACTION_REMOVE = 2;

const remove: IDiamondWritableInternal.FacetCutStruct[] = [{


target: ethers.constants.AddressZero,
action: FACET_CUT_ACTION_REMOVE,
selectors: ["0x1f931c1c"], // diamondCut.selector
}];

await this.diamond.diamondCut(remove, ethers.constants.AddressZero, "0x"); // fails


});

Mitigation
Consider adding the  diamondCut  function as an actual facet to the diamond. Alternatively, integrate a flag into an overridden
version of  diamondCut  in  KeepersDiamond  that would inhibit facet changes in the future.

Keepers Findings Details 6


[H-03] Potential for User Funds to be Stuck in ERC721r Contract Due to Expired
Commitments
Affected Functions
ERC721r

commit

_validateCommitForReveal

Description
The  ERC721r  contract employs a commit-reveal mechanism for ticket minting, with strict block window requirements for the
reveal transaction. The commit function allows users to commit funds for minting tickets, but the reveal function will revert if it
is not called within a range of  minCommitmentBlocks  to  maxCommitmentBlocks .
If a user does not or cannot call the reveal function before the window expires, the function will continue to revert for this
particular commitment. This effectively renders the committed funds inaccessible, as the funds cannot be retrieved nor the
tickets minted, potentially leading to a significant loss of funds.

// ERC721r.sol

function commit(uint128 numNFTs) public payable {


...
l.pendingCommits[msg.sender] = KeepersERC721Storage.MintCommit(numNFTs, uint128(block.number));
}

function _validateCommitForReveal(
KeepersERC721Storage.MintCommit memory pendingCommit,
bytes calldata rlpEncodedEntropyBlockHeader
) internal view {
...
if (block.number > pendingCommit.commitBlock + l.maxCommitmentBlocks) {
revert CommitmentTooOld();
}
...
}

Mitigation
This issue presents a significant risk to users, who could inadvertently lose funds due to missed or delayed reveal calls.
Consider implementing a mechanism to allow users to recover funds from expired commitments. A possible solution is to
add a function that cancels a commitment, refunding the user’s funds if the reveal phase has not been initiated within the
specified block window.

[H-04] Insufficient Access Control For Minting


Affected Functions
ERC721r

_mintRandomTokenId

Description
Upon examining the _mintAtIndex function, it appears that the project aims to restrict smart contracts from engaging in the
minting process and prohibits them from minting tickets.
This objective is accomplished through the implementation of the following require statement:

Keepers Findings Details 7


// ERC721r.sol

require(msg.sender == tx.origin, "Contracts cannot mint");

However, it should be noted that minting can also occur through the _mintRandomTokenId function, which does not include the
require statement. As a result, smart contracts have the capability to utilize this function for minting tokens.

Mitigation
There are two potential solutions to address this matter effectively:

1. Integrate the same require statement into the _mintRandomTokenId function.

2. Implement a modifier called onlyEOA and apply it to both functions. Alternatively, you can apply the modifier to the
external reveal function, which will initiate the minting logic.

[H-05] There is no Refund Function for Commits That Can’t be Fulfilled


Affected Contracts
ERC721r

Description
According to the project documentation and code, there are several situations in which a user may commit funds to
purchase a ticket but fail to receive it. These situations include the expiration of the maxCommitmentBlocks_ , the supply
exceeding the limit of 20,000 minted tokens, or the closure of the mint window.
To address this concern and enhance user trust, it is crucial to incorporate a function that enables users to reclaim their ETH
if they are unable to receive the ticket due to any of the aforementioned reasons. The implementation of this function should
align with the specifications outlined in the project documentation, which stated that in such case the user can receive a
refund for his ETH.
By including such a function, users will have the assurance that their funds can be retrieved if they encounter circumstances
preventing them from receiving the ticket. This will significantly contribute to building and maintaining user trust in the
project.

Mitigation
We recommend to implement a refundCommit function. This function will provide users with the ability to reclaim their ETH
ONLY if any of the described situations occur. By incorporating this refundCommit function into the system, users will have a
straightforward mechanism to retrieve their funds in case they are unable to receive the ticket due to the specified
circumstances.

[H-06] Tickets Can’t be Revealed and Converted to Avatars When The Mint
Window is Closed
Affected Functions
ERC721r

reveal

KeepersAvatarAssignment

convertTicketToAvatar

Description

Keepers Findings Details 8


The modifier whenMintWindowOpen is applied on following functions: commit , reveal , convertTicketToAvatar .

Applying this modifier to the reveal and convertTicketToAvatar functions introduces some challenges to the minting process
and overall user experience.
Consider the following scenario:

1. There are 15 minutes remaining for the sale window.

2. A user has committed 0.05 ETH to purchase a ticket.

3. The user attempts to invoke the reveal function:

a. If less than 15 minutes have passed - the transaction will succeed, and the user will receive the ticket.

b. However, if 15 minutes have already passed - the transaction will fail, and the user will be unable to receive the
ticket nor any refunt for the cimitted funds.

4. The user tries to convert their ticket to an Avatar by calling the convertTicketToAvatar function:

a. If less than 15 minutes have passed - the transaction will succeed, and the ticket will be successfully converted to an
avatar.

b. However, if 15 minutes have already passed - the transaction will fail, and the user will be unable to convert the
ticket to an avatar.

The issue arises when a user correctly initiates the minting and avatar customization process within the designated
timeframe (before the sale window closes), but becomes unable to complete the ticket claiming or avatar customization due
to the imposed modifier.
We believe that the two scenarios highlighted above are undesirable for the protocol and will have a negative impact on
users. Despite the user being "on time," they may not have sufficient time to complete the "reveal" and "convert to avatar"
processes.

Mitigation
We propose two potential solutions to address this issue:

1. Complete removal of the modifier from the reveal and convertTicketToAvatar functions.

2. Introduce an additional timeframe specifically for ticket reveal and avatar conversion. For instance, extending the
customization period to 96 hours while keeping the sale window at 72 hours. To implement this, a new modifier, such as
whenCustomizationWindowOpen , would be applied to the reveal and convertTicketToAvatar functions.

It is important to note that implementing the second solution would require making certain adjustments and modifications to
the system. This includes introducing new state variables and creating the aforementioned modifier.

[H-07] Inaccurate Rarity Distribution in Randomly Assigned Avatar


Configurations
Affected Contract
KeepersAvatarAssignment

getRandomWeightedRarityBitmap

getRandomTraitCount

Description
The vulnerability originates from the getRandomWeightedRarityBitmap function, where the assignment of avatar configurations
with different rarity traits occurs.

Keepers Findings Details 9


The implementation does not enforce the intended rarity distribution for all bitmap categories, leading to inaccurate
assignment of rare, uncommon, and common traits in addition to ultra rare traits:

function getRandomWeightedRarityBitmap(uint256 randNum) internal pure returns (uint256) {


uint256 rand = randNum % 1000;

if (rand < 5) {
// bitmap for ultra rare traits
return 101954143944505697739381706310682196132628916245778116988749700642598709444608;
} else if (rand < 20) {
// bitmap for rare traits
return 2720949656911802687759537703182600346823741657156241608343660096093389488128;
} else if (rand < 100) {
// bitmap for uncommon traits
return 3903317594296855677997402579119781559785639157701660181867509317632;
} else {
// bitmap for common traits
return 11116995631995377402132885316825708794697545202920566284662563087353521373184;
}
}
}

Similar issue happens in getRandomTraitCount , which will lead to deviation from the intended trait count distribution. The
function is responsible for determining the number of traits based on a random number, but the current implementation does
not guarantee the desired distribution percentages.

function getRandomTraitCount(uint256 largeRandNum) internal pure returns (uint8) {


uint256 randVal = largeRandNum % 1000;

if (randVal < 1) {
return 1;
} else if (randVal < 25) {
return 2;
} else if (randVal < 295) {
return 3;
} else if (randVal < 705) {
return 4;
} else if (randVal < 975) {
return 5;
} else if (randVal < 999) {
return 6;
} else {
return 7;
}
}

The impact of this vulnerability is a deviation from the intended rarity distribution among assigned avatar configurations. This
results in an improper scarcity and value representation of different traits, which can lead to dissatisfaction among users who
perceive the rarity and value of their avatars differently than anticipated.
Consider the following scenario:
For getRandomWeightedRarityBitmap :

1. The contract has 200 remaining tickets to be assigned avatars, with a desired rarity distribution of:

Ultra Rare: 0.5% (1 out of 200)

Rare: 1.5% (3 out of 200)

Uncommon: 8% (16 out of 200)

Common: 90% (180 out of 200)

2. The first 10 generated random numbers ( rand ) for assigning traits are as follows:

Keepers Findings Details 10


Rand 1: 3 (Ultra Rare)

Rand 2: 18 (Rare)

Rand 3: 22 (Uncommon)

Rand 4: 105 (Common)

Rand 5: 302 (Common)

Rand 6: 8 (Rare)

Rand 7: 91 (Uncommon)

Rand 8: 1 (Ultra Rare)

Rand 9: 419 (Common)

Rand 10: 208 (Common)

3. The assigned rarity distribution for the configurations, based on these rand values, is as follows for just the first 10
tokenIds:

Ultra Rare: 2 (Expected to be maximum 1)

Rare: 2

Uncommon: 2

Common: 4

4. The assigned distributions do not align with the desired rarity percentages, violating the intended rarity distribution for
avatar configurations.

For getRandomTraitCount :

1. The desired trait count distribution is as follows:

1 trait: 0.1% (1 out of 1000)

2 traits: 2.4% (24 out of 1000)

3 traits: 27% (270 out of 1000)

4 traits: 41% (410 out of 1000)

5 traits: 27% (270 out of 1000)

6 traits: 2.4% (24 out of 1000)

7 traits: 0.1% (1 out of 1000)

2. Ten random numbers ( largeRandNum ) are generated for trait count selection:

Random 1: 521

Random 2: 157

Random 3: 756

Random 4: 0

Random 5: 82

Random 6: 649

Random 7: 346

Random 8: 0

Random 9: 578

Keepers Findings Details 11


Random 10: 970

...

3. Based on these random numbers, the assigned trait counts for a sample of 10 tokens would be as follows:

Token 1: 3 traits

Token 2: 3 traits

Token 3: 4 traits

Token 4: 1 trait

Token 5: 2 traits

Token 6: 4 traits

Token 7: 3 traits

Token 8: 1 trait

Token 9: 3 traits

Token 10: 5 traits

...

4. The resulting trait counts do not align with the desired distribution, and the vulnerability becomes evident when
analyzing an even larger sample size.

Mitigation
To mitigate the vulnerabilities and ensure accurate distribution in assigned avatar configurations, the following steps can be
taken:
Randomize Ticket Assignments:

Create an array of all non-converted ticket ids and shuffle the array to introduce randomness in the ticket assignment
process.

Iterate through the shuffled array to assign avatar configurations, ensuring a more fair and unbiased distribution of
traits.

getRandomWeightedRarityBitmap :

Enforce Limits on Distribution: Implement explicit limits on the usage of bitmaps and traits according to their rarity.

Downgrade Mechanism: If a bitmap or trait count of greater rarity reaches its maximum usage limit, automatically
downgrade it to the next best rarity category.

getRandomTraitCount :

Enforce Limits on Trait Count: Implement explicit limits on the number of traits based on the desired distribution
percentages.

[H-08] A Malicious User Can Mint Tokens With Deterministic IDs


Affected Contract
ERC721r

reveal

Description

Keepers Findings Details 12


The protocol employs a commit-reveal design to mint tokens. Initially, users are required to call the commit function, in which
they pay 0.05 ETH. After a period of 5 blocks, they can invoke the reveal function, which in return, assigns them a token with
a seemingly random id. The random id is obtained from the header of the Xth block following the block where the commit
function was called (in our example the 5th block).

pendingCommit.commitBlock + l.minCommitmentBlocks;

Subsequently, the 13th element from the block header is extracted and labelled as randao .

// ERC721r.sol

RLPReader.RLPItem[] memory ls = rlpEncodedEntropyBlockHeader.toRlpItem().toList();


uint256 randao = ls[13].toUint();

The execution then moves into a specific loop:

// ERC721r.sol

for (uint i = 0; i < pendingCommit.numNfts; i++) {


bytes32 randHash = keccak256(abi.encodePacked(randao, i));
uint256 randomNumber = uint(randHash);
_mintRandomTokenId(msg.sender, randomNumber);
}

Here, for each iteration, the _mintRandomTokenId function is called for every commit a user has, passing it a random number
derived from the randao and the current iteration index.
The problem here is that the _mintRandomTokenId function will receive the exact same randomNum parameter for all users who
have called the commit function in the same block, for every iteration index.
To illustrate, if both Bob and Alice call the commit function for two tickets in block 20, and subsequently, call the reveal
function in block 25, they both enter the _mintRandomTokenId function with identical randomNum x for the first iteration and y for
the second iteration.
The protocol does attempt to inject further randomness into the process but falls short in execution.

// ERC721r.sol

uint256 randomIndex = randomNum % updatedNumAvailabledTokens;

Here, the randomIndex is calculated, representing the position where the token will be minted. This is done by obtaining the
modulus of the randomNum and the updatedNumAvailabledTokens , the current number of available tokens. However, this approach
doesn’t eliminate the problem, as the modulus operation can yield the same result with a constant dividend and a variable
divisor.
In the getAvailableTokenAtIndex function, if the randomIndex is already assigned as a token id, execution falls into the else
clause of the subsequent if-statement. Consequently, the id of the token minted is equivalent to the current count of free
tokens at the time of execution (e.g., 19999, 19998, 19997, etc.):

// ERC721r.sol

uint256 valAtIndex = l._availableTokens[indexToUse];


uint256 result;
if (valAtIndex == 0) {
result = indexToUse;
} else {
// @audit The result will get set here:

Keepers Findings Details 13


result = valAtIndex;
}

Given this flaw, a malicious actor could potentially exploit this by waiting until the number of available tokens hits a number
that is both a divisor of randomNum and a desired token id.

The following Python script demonstrates the calculation of all valid divisors of a given random number, highlighting the
potential for exploitation:

AMOUNT_OF_NFTS = 19999
INPUT_NUNMBER = int(input("Enter the random number: "))
entries = {}

def main() -> None:


for x in range(1, AMOUNT_OF_NFTS):
calc = INPUT_NUNMBER % x
if not calc in entries: entries[calc] = []
entries[calc].append(x)

ordered = sorted(entries.keys())

for x in ordered:
print(x,"= random %", entries[x])

if __name__ == "__main__":
main()

This could allow malicious users to target specific token ids, contrary to the system’s goal of random id allocation.
Consequently, this could be exploited to game the system, enabling users to claim unique or vanity token ids (e.g., 15000,
10000, 1, etc.)

Mitigation
To mitigate this issue, consider incorporating the sender’s address into the randomness generation to make collisions
significantly less likely.

// ERC721r.sol

bytes32 randHash = keccak256(abi.encodePacked(randao, msg.sender, i));

This strategy is also used in the CryptoPunks project to generate random ids.

// CryptoPunksV2

function useRandomAvailableToken(uint256 _numToFetch, uint256 _i)


internal
returns (uint256)
{
uint256 randomNum =
uint256(
keccak256(
abi.encode(
// @audit Here they use the msg.sender for generating the randomNum:
msg.sender,
tx.gasprice,
block.number,
block.timestamp,
blockhash(block.number - 1),
_numToFetch,
_i
)
)
);
uint256 randomIndex = randomNum % _numAvailableTokens;

Keepers Findings Details 14


return useAvailableTokenAtIndex(randomIndex);
}

[H-09] Protocol Design: There is No Incentive To Pay for Traits When You Can
Get Them For Free Once Minting Window is Closed
Description
it appears that users are required to pay for Traits when selecting them during the minting windows period. However, once
the minting window closes and the administrator assigns random configurations, users receive these traits "for free."
Considering the current implementation, users has no incentives for paying for trait selection if they can obtain them at no
cost once the minting window concludes.

Mitigation
One possible solution could involve implementing a fixed price that users must pay to the administrator / contract in order to
convert it into an avatar with a random configuration (after the minting window is closed)

Medium
[M-01] Inconsistent Role Identifier LICENSE_OPERATOR between KeepersDiamond.sol
and LicenseFacet.sol
Affected Functions
KeepersDiamond

constructor

LicenseFacet

setCommercialRightsOperator

revokeCommercialRightsOperator

setLicenseRevoked

Description
In  KeepersDiamond.sol , the role identifier  LICENSE_OPERATOR  is defined and granted:

// KeepersDiamond.sol

_grantRole(keccak256("LICENSE_OPERATOR"), msg.sender);

However, in  LicenseFacet.sol , another role identifier  LICENSE__OPERATOR  (with two underscores) is used instead due to a typo:

// LicenseFacet.sol

bytes32 constant LICENSE_OPERATOR = keccak256("LICENSE__OPERATOR");

As a result, all role checking in  LicenseFacet.sol , specifically in the  setLicenseRevoked  function, will not work as expected.
This is because the mismatch in role identifiers will cause the checks for  LICENSE_OPERATOR  to fail in  LicenseFacet.sol :

Keepers Findings Details 15


// LicenseFacet.sol

function setLicenseRevoked(uint256 tokenId, bool isRevoked) external onlyOwner {


LicenseStorage.Layout storage l = LicenseStorage.layout();
if (!_hasRole(LICENSE_OPERATOR, msg.sender)) {
revert NotLicenseOperator(msg.sender);
}
...
}

Mitigation
This is considered a medium-severity issue since  LicenseFacet.sol  does provide methods for updating role assignments,
which can be used to rectify this problem.
However, to prevent further issues and maintain consistency, consider rectifying the role identifier typo in  LicenseFacet.sol .
Change  LICENSE__OPERATOR  to  LICENSE_OPERATOR  to match the role identifier defined in  KeepersDiamond.sol .

[M-02] Inconsistency in Maximum Tickets Allowed Per Wallet


Affected Functions
KeepersDiamond

constructor

Description
In  KeepersDiamond.sol , the  maxPerAddress  field of  KeepersERC721Storage  is initialized with a value of 10:

// KeepersDiamond.sol

KeepersERC721Storage.layout().maxPerAddress = 10;

However, the documentation states that:

Each wallet can have a max of 100 tickets

This is a discrepancy between the code implementation and the provided documentation.

Mitigation
It’s crucial for the documentation to accurately reflect the implementation to avoid misunderstandings and potential misuse of
the system.
Align the  maxPerAddress  value in  KeepersDiamond.sol  with the value stated in the documentation. That is,
change  maxPerAddress  from 10 to 100. Alternatively, if the implemented limit of 10 is the intended behavior, update the
documentation accordingly to prevent confusion.

[M-03] Risk of Lost Tokens due to Use of  _mint  Instead of  _safeMint
Affected Functions
ERC721r

_mintRandomTokenId

_mintAtIndex

Keepers Findings Details 16


Description
The  ERC721r  contract is responsible for minting ERC721 tokens. In the  _mintRandomTokenId  and  _mintAtIndex  methods, the
contract uses the  _mint  function instead of  _safeMint .
In the case where the recipient of these ERC721 tokens is a contract, and that contract is not equipped to handle incoming
ERC721 tokens, these tokens could become inaccessible and essentially lost.

// ERC721r.sol

function _mintRandomTokenId(address to, uint256 randomNum) internal virtual returns (uint256) {


...
_mint(to, tokenId);
...
}

function _mintAtIndex(address to, uint index) internal virtual {


...
_mint(to, tokenId);
}

Mitigation
To avoid this risk, it is recommended to use the  _safeMint  function in place of  _mint . The  _safeMint  function is designed to
ensure that the recipient is capable of receiving ERC721 tokens, and will revert the transaction if not.
This modification would look something like this:

// ERC721r.sol

function _mintRandomTokenId(address to, uint256 randomNum) internal virtual returns (uint256) {


...
_safeMint(to, tokenId);
...
}

function _mintAtIndex(address to, uint index) internal virtual {


...
_safeMint(to, tokenId);
}

It is important to highlight that modifying it to _safeMint in the contract will result in an external call to the “to” address,
potentially introducing a vulnerability of ReEntrnacy Attacks in the system. Therefore, it is strongly recommended to
consistently follow to the CEI pattern.

[M-04] Fixed Size Array Limits Upgradability of  RoomNamingFacet  in Diamond
Proxy Pattern
Affected Functions
RoomNamingStorage

layout

Description
The  RoomNamingStorage  contract uses a fixed-length array of strings  customRoomNames  of length 10. If in the future, the project
needs to store more than 10  customRoomNames , they won’t be able to do so.
The Diamond pattern is utilized in the Keepers project to allow upgradability. However, the mentioned structure hinders
upgradability because if the structure of this storage is changed in a future version of the facet, it could result in data
corruption.

Keepers Findings Details 17


If you add, remove, or change the type of a variable in this structure, it could lead to overwriting of the next storage
variables.

// RoomNamingStorage.sol

library RoomNamingStorage {
...
struct Layout {
...
string[10] customRoomNames;
...
}
...
}

Mitigation
The mitigation of this issue involves refactoring the  customRoomNames  from a fixed size array to a mapping along with a counter,
or even a dynamic array.
This will ensure that you have flexibility and can add as many  customRoomNames  as you want in the future. If a dynamic array is
used, a method to get the length of the array must be created as Solidity doesn’t provide this for arrays in storage by default.
Below is a possible alternative implementation:

// RoomNamingStorage.sol

library RoomNamingStorage {
...
struct Layout {
...
Counters.Counter _customRoomNamesCount; // added counter for customRoomNames
mapping(uint256 => string) customRoomNames; // changed from fixed size array to mapping
...
}
...
}

While this solution resolves the upgradability issue, it does introduce additional complexity as it involves managing the
counter for the number of  customRoomNames , and necessitates the creation of methods to access and manipulate the names in
the mapping.

[M-05] Multiple Methods are Marked as  Payable  but Do Not Receive Any Ether
Affected Functions
ERC721Base

approve

transferFrom

safeTransferFrom(address from, address to, uint256 tokenId)

safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)

ERC721r

reveal

KeepersERC721

adminMintTickets

Keepers Findings Details 18


RoomNamingFacet

createSpecialTickets

Description
Multiple functions are marked as  payable  but do not receive any Ether (see Affected Functions). This can lead to accidental
loss of funds if the caller sends Ether to these functions.

Mitigation
Remove the  payable  modifier from the functions listed above.

[M-06] Incorrect Base64 Data URL Scheme Used


in  _tokenURI  of  KeepersERC721Metadata
Affected Functions
KeepersERC721Metadata

_tokenURI

Description
In the  _tokenURI  function of  KeepersERC721Metadata , the base64 data URL scheme is used to encode the token URI. However,
this is incorrect because the token URI is not base64 encoded data. Instead, it is a standard JSON string converted to bytes.

// KeepersERC721Metadata.sol

function _tokenURI(uint256 tokenId) {


...
return string(abi.encodePacked("data:application/json;base64,", specialTicketData));
...
return string(abi.encodePacked("data:application/json;base64,", standardTicketData));
...
return string(abi.encodePacked("data:application/json;base64,", avatarData));
}

Mitigation
To rectify this issue, the  ;base64  part in the returned string should either be removed, or the returned data should be
encoded using a base64 encoder library like OpenZeppelin’s  Base64  library, it should look like

using Base64 for bytes;

return string(abi.encodePacked("data:application/json;base64,", avatarData.base64()));

[M-07] Usage of transfer Instead of call in KeepersAdminFacet


Affected Functions
KeepersAdminFacet

withdraw

Description
The  withdraw  function in the  KeepersAdminFacet  contract uses  transfer  to send ETH:

Keepers Findings Details 19


// KeepersAdminFacet.sol

function withdraw(address payable recipient) public onlyOwner {


if (recipient == address(0)) {
revert ZeroAddressReceiver(recipient);
}
uint256 balance = address(this).balance;
recipient.transfer(balance);
}

While  transfer  was designed to prevent reentrancy attacks, it may lead to complications if the recipient is a smart contract.
A withdrawal will fail if:

1. The smart contract receiving the refund doesn’t implement a payable fallback function

2. The implemented payable fallback function uses more than 2300 gas units

3. The implemented payable fallback function uses less than 2300 gas units, but a proxy elevates the call’s gas usage
beyond 2300

This might restrict the kinds of contracts that can interact with this contract and would be a severe limitation if those
interactions are desired in the future.

Mitigation
Given that the  withdraw  function can only be called by the owner, the risk of reentrancy attacks is low. We recommend
replacing  transfer  with a direct call as it has higher gas stipend and allows the recipient to execute a more complex
fallback function if needed.

It also ensures that any contract can receive funds from this contract as long as they implement a payable fallback function,
regardless of its gas consumption.
Alternatively, you can use the sendValue function which is available in the solidstate AddressUtils.sol library (which uses the
call opcode).

[M-08] The Diamond Contract Deployer is a Single point of failure without any
Mitigation
Affected Contracts

KeepersDiamond

Description
The msg.sender address, which represents the deployer of the KeepersDiamond contract, holds significant power, and any
potential compromise of this address could lead to severe consequences.
The deployer address, serving as the sole entity, possesses the following roles and associated benefits:

Owner of the Diamond Proxy:

Authorized to add, remove, and update facets

Permitted to invoke functions protected by the onlyOwner modifier

KEEPERS_TERMS_OPERATOR role

LICENSE_OPERATOR role

Entitled to receive NFT royalties

Given the sensitive nature of these functionalities, a compromise of this address would jeopardize the entire system's
security.

Keepers Findings Details 20


Mitigation
To mitigate risks, it is highly recommended to assign different permissions to various unique addresses based on their roles,
especially considering that the AccessControl library is already implemented and distinct roles have been defined.

[M-09] The Requirement That The Call is From an EOA Might Not Hold True In
The Future
Affected Contracts

ERC721r

_mintAtIndex

Description
This require statement in the _mintAtIndex function is used to ensure calls are only made from externally owned accounts
(EOAs):

// ERC721r.sol

require(msg.sender == tx.origin, "Contracts cannot mint");

However, the validity of using msg.sender == tx.origin as a means to ensure that calls originate only from EOAs may be
questioned, according to EIP 3074.
EIP 3074 proposes the introduction of two new EVM instructions: AUTH and AUTHCALL .
The AUTH instruction sets an authorized context variable based on an ECDSA signature, while the AUTHCALL instruction
allows for making a call on behalf of the authorized account. Essentially, this EIP enables smart contracts to assume control
over an externally owned account (EOA). If EIP 3074 is implemented, this check will no longer be accurate.

Mitigation
In addition to the existing verification process, it is advisable to utilize OpenZeppelin's Address Library's isContract function.
It is important to note that there are certain exceptional scenarios, such as contracts being instantiated within constructors,
which can potentially circumvent this verification. Therefore, caution should be exercised when employing this approach.

[M-10] State Can Be Changed When The System is Paused


Affected Functions
KeepersDiamond (SolidStateDiamond → DiamondWritable)

diamondCut

KeepersAvatarAssignment

CreateTraitsBulk

bulkAssignRandomAvatarConfigs

ERC721Base

KeepersERC721

adminMintTickets

LicenseFacet

setCommercialRightsOperator

Keepers Findings Details 21


revokeCommercialRightsOperator

setLicenseRevoked

RoomNamingFacet

createSpecialTickets

getRoomName

TermsFacet

setTermsPart

setTermsOperator

revokeTermsOperator

Description
The project uses the Pausable contract and the whenNotPaused modifier to enforce the prevention of state changes while the
system is in a paused state as set by the administrators.
Nevertheless, there exist several functions in the system that can modify its state without the whenNotPaused modifier being
applied to them. These functions are listed in the affected functions section.
This practice introduces inconsistencies in the system behavior and compromises security by allowing access and state
changes during the paused state.

Mitigation
Apply the whenNotPaused modifer to the listed functions.

[M-11] Sale Settings Are Mutable During The Sale Phase


Affected Functions
KeepersAdminFacet

setSaleStartTimestamp

setSaleCompleteTimestamp

setMaxPerAddress

Description
The functions referenced above are designed to be triggered by the owner during the setup phase of the system.
However, they can also be triggered during the sale phase. This implementation introduces potential risks such as
unexpected behavior and a centralized control that may undermine the trust of users who wish to participate in the Keepers
Sale.
Allowing the admins to modify the sale settings while the sale is ongoing raises concerns regarding trust and can erode the
confidence of users in the project.

Mitigation
Restrict the usage of the mentioned functions to the setup phase only (for the initial sale configuration). If the values have
already been set, administrators should not be permitted to modify them and the transaction should be reverted. This
ensures that the sale settings remain unchanged once established, preventing unauthorized alterations and maintaining
consistency.

Keepers Findings Details 22


[M-12] Lack of Proper Unit Testing and Documentation
Description
Ensuring comprehensive unit test coverage is essential for system testing and forms the fundamental groundwork prior to
conducting fuzz testing and manual smart contract audits.
In this particular case, the unit tests were incomplete and exhibited several issues:

Numerous tests were broken.

Many tests were missing, and they did not encompass the recent codebase refactoring and diamond proxy
implementation.

Testing the code through the proxy rather than directly on the facet contracts is critical. This approach helps detect critical
vulnerabilities such as improper initialization of state variables and contract access issues, which may otherwise be
overlooked.
Furthermore, the absence of well-defined project specifications documentation posed challenges during the contract audit.
We encountered difficulties in understanding some basic aspects of the project, limiting our ability to effectively review the
contracts.
An organized doc with all the aspects of the project is essential:

How are traits categorized within categories.

Simplified explanation of the bitmap traits implementation.

How the rarity system works.

Special Tickets and their benefits.

Minting rules, including the start and end dates, maximum per wallet, minimum commit block, maximum commit block,
etc.

Mitigation
Prior to the launch, we strongly recommend the following steps:

1. Develop comprehensive documentation that encompasses the entire scope of the project, ensuring its availability to
users.

2. Complete the coverage of unit tests and conduct testing on the contract using the diamond proxy instead of testing
directly on the facets. It is crucial to verify that all tests pass successfully.

[M-13] No Proper Validation for minCommitmentBlocks and maxCommitmentBlocks

Affected Functions
KeepersDiamond

constructor

Description
The parameters minCommitmentBlocks and maxCommitmentBlocks play a critical role in the minting process.
If minCommitmentBlocks is set to a value lower than 5, it can jeopardize the randomness of the tokenId assignment functionality.
On the other hand, if maxCommitmentBlocks exceeds 256, it will result in a Denial of Service (DoS) scenario during the reveal
process. This is because the blockhash function can retrieve the block hash of the last 256 blocks only.
These two significant parameters are currently being assigned in the KeepersDiamond constructor without undergoing any form
of validation:

Keepers Findings Details 23


// KeepersDiamond.sol

KeepersERC721Storage.layout().minCommitmentBlocks = minCommitmentBlocks_;
KeepersERC721Storage.layout().maxCommitmentBlocks = maxCommitmentBlocks_;

Furthermore, in the event of misconfiguration of these parameters, the admin would face a challenge in updating them due
to the absence of setter functions in any of the facets. Once these parameters are set, they cannot be modified directly.
The only viable solution in such case would involve adding a new facet that include setters for these parameters.

Mitigation
We strongly recommend incorporating validation for the minCommitmentBlocks and maxCommitmentBlocks parameters within the
KeepersDiamond constructor. This validation can be implemented as follows:

if(minCommitmentBlocks < 5 || minCommitmentBlocks > 20) revert WrongMinCommitmentBlocks();


if(maxCommitmentBlocks < 100 || maxCommitmentBlocks > 256) revert WrongMaxCommitmentBlocks();

[M-14] Implementated Code Does Not Align with the Provided Documentation
Affected Functions
KeepersAvatarAssignment

createTraitsBulk

findWeightedRandomAvatarConfig

Description
In the documentation it is specified the following:

Avatar Traits are created on chain (there are a total of 244 traits).

However, there are two places in the code where this is not followed.
First, inside createTraitsBulk() :

// KeepersAvatarAssignment.sol

if (traitId < 1 || traitId > 242) revert InvalidTraitId(traitId);

In one particular scenario, there is a risk that the traitId with a value of 243 or 244 may not proceed as expected, potentially
resulting in a reverted transaction.
Additionally, this principle is not followed within the function findWeightedRandomAvatarConfig() .

// KeepersAvatarAssignment.sol

// get a trait id from the bitmap


uint256 traitId = PseudoRandomLib.findRandomSetBitIndex(bitmapToSearch, randNum, 243);

Another deviation from the documentation is observed in this case, where the parameter maxIndex passed is set to 243,
which does not align with the specified correct amount.

Mitigation

Keepers Findings Details 24


There should be consensus and alignment regarding the specific number of expected traits.

[M-15] Potential Denial of Service Due to The 72 Hours Window for Minting Not
Being Initialized
Affected Functions
KeepersMintWindowModifiers

_isMintingWindowOpen

whenMintWindowOpen

KeepersAvatarAssignment

convertTicketToAvatar

ERC721r

commit

reveal

Description
In order to use convertTicketToAvatar() reveal() and commit() functions it is necessary that the window for minting is open.
That is checked through the modifier whenMintWindowOpen() which is calling the function _isMintingWindowOpen() .
Inside the _isMintingWindowOpen() function it is verifying the following:

// KeepersMintWindowModifiers.sol

return block.timestamp >= l.saleStartTimestamp && block.timestamp <= l.saleCompleteTimestamp;

While there are existing setters inside KeepersAdminFacet.sol , there is no default value added on the KeepersDiamond

constructor. There’s, however, just an existing TODO note

// KeepersDiamond.sol

// TODO - set up sale parameters


// uint256 public saleStartTimestamp = block.timestamp + 1000 days; // Need to set actual value
// uint256 public saleCompleteTimestamp = block.timestamp + 5000 days; // Need to set actual value

Upon deployment, the default value of these two variables will be set to zero. This poses a significant risk of causing a
Denial of Service (DoS) situation for any user attempting to use all the functions which are depende on these state variable..

Mitigation
Make sure both variables are properly initialized on KeepersDiamond.sol constructor.

[M-16] Wrong IMG Base URL In _tokenURI function


Affected Functions
KeepersERC721Metadata

_tokenURI

Description

Keepers Findings Details 25


The _tokenURI function retrieves all the relevant data for a given token ID, including the PATH to the token's image. The
image path is constructed by concatenating the following components:
baseURI + TicketType + TokenID

The TicketType can take two possible values: STANDARD_TICKET_PATH or SPECIAL_TICKET_PATH , both of which are hardcoded
constants representing /standardTicket and /specialTicket , respectively.
However, an issue arises during the concatenation process where a forward slash ( / ) is missing between the TicketType

and the TokenID . As a result, the resulting concatenation appears as:


https://keepers.xyz/standardTicket5231

Instead of the correct form:


https://keepers.xyz/standardTicket/5231

This discrepancy may lead to an invalid path and a filure to load the ticket’s image.

Mitigation
Include the missing slash in the STANDARD_TICKET_PATH and SPECIAL_TICKET_PATH constants. These constants should be updated
to /standardTicket/ and /specialTicket/ .

[M-17] Broken IMGs URI for Special Tickets Converted to Avatars


Affected Functions
KeepersERC721Metadata

_tokenURI

Description
Once a token has been converted to an Avatar, the _tokenURI function retrieves additional data associated with the assigned
avatar traits for the given ticket. This retrieval process involves an if statement that checks whether the token has already
been converted to an avatar ( if (convertedState == 0) ).
When the token is successfully converted to an avatar, the function proceeds to assemble and return a JSON object
containing all the relevant data pertaining to this particular avatar, including the Avatar IMG path.
However, the is an issue with the assembly of the avatar's IMG path. Currently, the path is always constructed using the
STANDARD_TICKET_PATH constant.

When attempting to retrieve information for a Special Ticket that has been converted to an avatar, the resulting IMG Path will
be incorrect. As a result, the image will fail to load because instead of yielding the expected URI
https://keepers.xyz/specialTicket/[special_ticket_id] , it will produce the URI

https://keepers.xyz/standardTicket/[special_ticket_id] , which likely does not exist on the server.

Mitigation
A solution can be implemented by following a consistent approach like the one with the tickets that weren’t converted to
Avatars yet. By adding an if statement, we can determine whether the ticket is a special or standard ticket, and accordingly
assemble the IMG Path.
To further optimize the implementation, the image URI can be calculated once and utilized regardless of whether the ticket
has been converted to an avatar or not. Since the image URI will remain the same in both cases, this approach eliminates
the need for duplicate code and ensures consistency throughout the process.

[M-18] Unexpected Revert Due to a Chance of ID Collission


Affected Functions

Keepers Findings Details 26


ERC721r

reveal

Description
The reveal function employs the subsequent code block to identify the id it will mint.

// ERC721r.sol

uint256 valAtIndex = l._availableTokens[indexToUse];


uint256 result;
if (valAtIndex == 0) {
result = indexToUse;
} else {
result = valAtIndex;
}

This segment initially examines whether the indexToUse is vacant. If not, it assigns the ‘result’ to the current quantity of
unoccupied ids.
Subsequently, the function attempts to mint the result variable.

// ERC721r.sol

uint256 tokenId = getAvailableTokenAtIndex(randomIndex, updatedNumAvailableTokens);


_mint(to, tokenId);

The fundamental problem here lies in the absence of checks ensuring the valAtIndex constitutes a vacant token id. If it is
not, the transaction will oddly revert, apparently without any cause, leading the user to forfeit the gas for that specific
transaction. This could also potentially harm the protocol’s reputation, as users will be in the dark about the reason for this
unusual occurrence.
The probability of this id collision increases with each minted token and becomes significantly likely when the count of
available ids approaches a 3-digit number. Furthermore, due to the problem highlighted in [H-08], the collision risk escalates
significantly for users who have invoked the ‘commit’ function within the same block.

Mitigation
Consider implementing a validation step for the valAtIndex to confirm it doesn’t correspond to an already claimed token. If it
does, another index should be selected by leveraging an approach akin to the logic used in the getAvailableTokenAtIndex
function.

[M-19] Reverse Bitmaps Causes a User’s Random Trait Selection to Get


Harmed
Affected Functions
KeepersAvatarAssignment

findWeightedRandomBodyTrait

Description
The function findWeightedRandomBodyTrait is responsible for checking whether the variable bitmapToSearch is equal to zero. In
such a scenario, the function currently defaults to returning a standard black body. The issue lies in the usage of the bitwise
AND ( & ) operator, which can yield a result of zero when applied to two binary values that are mirror images of each other
(e.g., 010101 and 101010):

Keepers Findings Details 27


// KeepersAvatarAssignment.sol

function findWeightedRandomBodyTrait(
uint256 cumulativeValidTraitsBitmap,
uint256 randSeed
) internal pure returns (uint256) {
uint256 randNum = PseudoRandomLib.deriveNewRandomNumber(randSeed);
uint256 rarityBitmap = getRandomWeightedRarityBitmap(randNum);

uint256 bitmapToSearch = cumulativeValidTraitsBitmap & rarityBitmap;

// in case no traits are available, default to the basic black body


if (bitmapToSearch == 0) return BLACK_BODY_ID;

// fetch a trait id from the bitmap (first 8 traits are bodies)


return PseudoRandomLib.findRandomSetBitIndex(bitmapToSearch, randNum, 8);
}

Considering that the rarity bitmap can have four possible values, it is possible to determine their binary mirror counterparts
straightforwardly. If the bulkAssignRandomAvatarConfigs function processes a token and the value of cumulativeValidTraitsBitmap
corresponds to the binary inverse of the current rarity, the token will default to the generic black body instead of potentially
receiving a randomly assigned body trait.

Since users have the ability to influence the cumulativeValidTraitsBitmap by generating avatars with specific traits, a malicious
actor could exploit this behavior, resulting in the described scenario occurring with a 25% probability.

Mitigation
A potential mitigation approach is to enhance the existing if statement by including an additional condition. This condition
would check whether both bitmapToSearch AND cumulativeValidTraitsBitmap are zero.

By verifying both variables simultaneously, it can help ensure that the defaulting behavior to the generic black body is only
triggered when both conditions are met:

if (bitmapToSearch == 0 && cumulativeValidTraitsBitmap == 0) return BLACK_BODY_ID;

Low
[L-01] No Checks on Start and Completion Time in Mint Window
Affected Functions
KeepersERC721

adminMintTickets(uint256 _count)

ERC721r

commit(uint128 numNFTs)

reveal(bool agreeToTermsOfService, bytes calldata rlpEncodedEntropyBlockHeader)

KeepersMintWindowModifiers

_isMintingWindowOpen()

whenMintWindowOpen()

whenMintWindowClosed()

KeepersAdminFacet

Keepers Findings Details 28


setSaleStartTimestamp(uint256 timestamp)

setSaleCompleteTimestamp(uint256 timestamp)

Description
In  KeepersAdminFacet , two timestamps,  saleStartTimestamp  and  saleCompleteTimestamp , can be set and modified
via  setSaleStartTimestamp(uint256 timestamp)  and  setSaleCompleteTimestamp(uint256 timestamp) .

// KeepersAdminFacet.sol

function setSaleStartTimestamp(uint256 timestamp) external onlyOwner {


KeepersERC721Storage.layout().saleStartTimestamp = timestamp;
}

function setSaleCompleteTimestamp(uint256 timestamp) external onlyOwner {


KeepersERC721Storage.layout().saleCompleteTimestamp = timestamp;
}

These timestamps determine the minting window for tickets, during which  commit  and  reveal  functions in  ERC721r.sol  can be
invoked, and  adminMintTickets  in  KeepersErc721.sol  can be executed when the window is closed.
In the  KeepersMintWindowModifiers  contract, the checks  whenMintWindowOpen  and  whenMintWindowClosed  are based on the
assumption that  saleStartTimestamp  is always before  saleCompleteTimestamp .

// KeepersMintWindowModifiers.sol

function _isMintingWindowOpen() internal view returns (bool) {


KeepersERC721Storage.Layout storage l = KeepersERC721Storage.layout();
return block.timestamp >= l.saleStartTimestamp && block.timestamp <= l.saleCompleteTimestamp;
}

However, there is no explicit check in the  setSaleStartTimestamp  and  setSaleCompleteTimestamp  functions to


prevent  saleCompleteTimestamp  from being set before  saleStartTimestamp .

This could potentially lead to unexpected behaviors.

Mitigation
This is considered a low-severity issue as the functions are called by the owner only. However, to prevent possible mistakes,
consider adding a check in the  setSaleStartTimestamp  and  setSaleCompleteTimestamp  functions to ensure  saleCompleteTimestamp  is
always after  saleStartTimestamp .

// Suggested code

function setSaleStartTimestamp(uint256 timestamp) external onlyOwner {


if (timestamp >= KeepersERC721Storage.layout().saleCompleteTimestamp) {
revert SaleStartTimeMustBeBeforeEndTime();
}
KeepersERC721Storage.layout().saleStartTimestamp = timestamp;
}

function setSaleCompleteTimestamp(uint256 timestamp) external onlyOwner {


if (timestamp <= KeepersERC721Storage.layout().saleStartTimestamp) {
revert SaleEndTimeMustBeAfterStartTime();
}
KeepersERC721Storage.layout().saleCompleteTimestamp = timestamp;
}

[L-02] Missing zero-address Check

Keepers Findings Details 29


Affected Functions
LicenseFacet.sol

setCommercialRightsOperator

revokeCommercialRightsOperator

Description
Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly.

Mitigation
Consider adding zero-address checks in the mentioned functions require(operator != address(0)); .

[L-03] Open TODOs


Affected Functions
KeepersDiamond

L50-52

L62

KeepersAvatarAssignment

L14

L375

KeepersAvatarAssignmentStorage

L16

ERC721r.sol

_mintRandomTokenId

Description
Several smart contracts contain open TODOs that require attention, such as:

// KeepersDiamond.sol

// TODO - set up sale parameters


// uint256 public saleStartTimestamp = block.timestamp + 1000 days; // Need to set actual value
// uint256 public saleCompleteTimestamp = block.timestamp + 5000 days; // Need to set actual value

// TODO - set up operator filter subscription

Open TODOs can suggest programming or architectural errors that still need to be addressed.

Mitigation
We recommend resolving all the open TODOs before the mainnet launch.

[L-04] Potential ReEntrancy Attacks Due to not following the CEI Pattern
Affected Functions
ERC721R

Keepers Findings Details 30


Reveal

_mintRandomTokenId

Description
There are functions within the system that do not adhere to the CEI (Check-Effect-Interactions) Pattern:

reveal function, minting tokens before updating the KeepersERC721Storage layout:

// ERC721r.sol

function reveal(
bool agreeToTermsOfService,
bytes calldata rlpEncodedEntropyBlockHeader
) external payable nonReentrant whenMintWindowOpen whenNotPaused {
KeepersERC721Storage.Layout storage l = KeepersERC721Storage.layout();
if (!agreeToTermsOfService) {
revert MustAgreeToTermsAndConditions();
}

KeepersERC721Storage.MintCommit memory pendingCommit = l.pendingCommits[msg.sender];


_validateCommitForReveal(pendingCommit, rlpEncodedEntropyBlockHeader);

// extract the randao (entropy) from the entropy block header


// this is exposed as the "mixHash" which is the 13th value in
// the rlpEncoded header
RLPReader.RLPItem[] memory ls = rlpEncodedEntropyBlockHeader.toRlpItem().toList();
uint256 randao = ls[13].toUint();

for (uint i = 0; i < pendingCommit.numNfts; i++) {


bytes32 randHash = keccak256(abi.encodePacked(randao, i));
uint256 randomNumber = uint(randHash);
_mintRandomTokenId(msg.sender, randomNumber);
}

// clear the pending commit and reclaim gas


delete l.pendingCommits[msg.sender];

// increase the mint count for the user


l.mintCountPerAddress[msg.sender] += pendingCommit.numNfts;
}

_mintRandomTokenId function, executing _mint before updating KeepersERC721Storage :

// ERC721r.sol

function _mintRandomTokenId(address to, uint256 randomNum) internal virtual returns (uint256) {


// @audit-issue - can remove this check to save gas, to is always msg.sender
require(to != address(0), "ERC721: mint to the zero address");

KeepersERC721Storage.Layout storage l = KeepersERC721Storage.layout();

uint updatedNumAvailableTokens = l._numAvailableTokens;


uint256 randomIndex = randomNum % updatedNumAvailableTokens; // TODO - this seems like a bug that will choose too low an index?
uint256 tokenId = getAvailableTokenAtIndex(randomIndex, updatedNumAvailableTokens);

// @audit Use safemint? even though they limit only EOA accounts?
_mint(to, tokenId);

--updatedNumAvailableTokens;
l._numAvailableTokens = updatedNumAvailableTokens;

return tokenId;
}

It is important to consider the following:

Keepers Findings Details 31


1. Applying the nonReentrany modifier to the functions does not completely mitigate the need for following the pattern, as
potential cross-function or cross-contract reentrancy issues may still arise.

2. Even if a function currently lacks external calls, there is a possibility of future additions (e.g., using the _safeMint function
instead of mint ) that would introduce external calls.

Therefore, it is always recommended to consistently follow to the CEI pattern.

Mitigation
Follow the CEI pattern in the mentioned functions above.

[L-05] Inconsistency in Solidity Compiler Versions and Floating Pragrma


Affected Functions
All the contracts

Description
The smart contracts employed in the project utilize floating pragmas, for example:

// KeepersERC721Storage.sol

pragma solidity ^0.8.8;

Furthermore, there exists inconsistency in the Solidity compiler versions across the smart contracts within the project. This
can be observed in instances such as:

// KeepersERC721.sol

pragma solidity ^0.8.9;

// KeepersERC721Storage.sol

pragma solidity ^0.8.8;

// KeepersAdminFacet.sol

pragma solidity ^0.8.12;

KeepersDiamond.sol -> ^0.0.8


KeepersAvatarAssignment.sol -> ^0.8.9
KeepersAvatarAssignmentStorage.sol -> ^0.8.8
KeepersAdminFacet.sol -> ^0.8.12
ERC721r.sol -> ^0.8.9
ERC721Base.sol -> ^0.8.8
KeepersERC721Metadata.sol -> ^0.8.9
KeepersERC721.sol -> ^0.8.9
KeepersERC721Storage.sol -> ^0.8.8
KeepersERC721Metadata.sol -> ^0.8.8
KeepersMintWindowModifiers.sol -> ^0.8.9
LicenseFacet.sol -> ^0.8.12
LicenseStorage.sol -> ^0.8.8
RoomNamingFacet.sol -> ^0.8.8
RoomNamingStorage.sol -> ^0.8.8
TermsFacet.sol -> ^0.8.12
TermsStorage.sol -> ^0.8.8

Keepers Findings Details 32


ConfigLib.sol -> ^0.8.9
PseudoRandomLib.sol -> ^0.8.9

To ensure consistency and mitigate potential issues, it is recommended to deploy contracts using the same compiler version
and flags that were utilized during their development and testing stages.
By specifying the pragma explicitly, the risk of inadvertently deploying contracts with a different pragma version is minimized.
This is crucial because using an outdated pragma version can introduce bugs that have a negative impact on the contract
system, while recently released pragma versions may have undiscovered security vulnerabilities. In the present scenario,
the contracts fail to compile with the older versions.

Mitigation
Consider locking the pragma in all the contracts to the 0.8.12 version.
It is not recommended to use a floating pragma in production.

[L-06] Missing Events in State Changing and Critical Functions


Affected Functions
KeepersDiamond

constructor

KeepersAdminFacet

setSaleStartTimestamp

setSaleCompleteTimestamp

setMaxPerAddress

withdraw

setBaseURI

KeepersERC721

adminMintTickets

ERC721r

commit

reveal

LicenseFacet

setLicenseRevoked

TermsFacet

setTermsPart

setTermsOperator

revokeTermsOperator

Description
Several state-changing and critical admin functions don’t emit events. Emitting events provide transparency and traceability
to any changes made in the state of the system.

As such, it is generally considered a best practice to emit events with every function that modifies a contract’s state.

Keepers Findings Details 33


Mitigation
We recommend introducing an event for each state-changing function. By doing so, you can provide external observers with
a detailed history of the contract’s operation, thus improving transparency. This would also make it easier for front-ends and
off-chain services to react to state changes.

Consider adding timelocks as well so that users and other privileged roles can detect upcoming changes (by off-chain
monitoring of events) and have the time to react to them.

[L-07] Using block.difficulty to Retrieve Pseudo-Random Numbers is


Considered a Deprecated Approach
Affected Functions
PseudoRandomLib

getPseudoRandomNumber

Description
The getPseudoRandomNumber() function currently retrieves a pseudo-random number by using block.difficulty .
As outlined in EIP-4399, it is crucial to note that block.difficulty has been deprecated and is currently maintained solely for
backward compatibility purposes. Therefore, it is strongly advised to replace the usage of block.difficulty with
block.prevrandao as a more appropriate alternative.

Mitigation
We recommend replacing the usage of block.difficulty with block.prevrandao .

[L-08] Remove unused functions such as _mintAtIndex

Affected Functions
ERC721r

_mintAtIndex

Description
The internal function _mintAtIndex is currently implemented but not utilized by any other function within the codebase. To
enhance code readability and optimize gas efficiency, it is advised to remove unused functions.

Mitigation
Either use or remove the _mintAtIndex function.

[L-09] Should Allow Approved Accounts to Convert Tickets to Avatars


Affected Functions
KeepersAvatarAssignment

convertTicketToAvatar

Description
The convertTicketToAvatar function enables the owner of a minted token (ticket) to transform it into an avatar. To ensure that
only the ticket owner can trigger this conversion, a validation check is implemented within the function. This validation

Keepers Findings Details 34


ensures that only the rightful owner of the ticket, associated with a specific token ID, can initiate the conversion process to
obtain an avatar:

// KeepersAvatarAssignment.sol

if (_ownerOf(tokenId) != msg.sender) revert NotTokenOwner(msg.sender, tokenId);

In certain scenarios where users interact with the keepers dApp through a contract (e.g., Escrow contracts, DAOs, etc.),
there may arise a situation where the intermediate smart contract is unable to execute the conversion of the ticket to an
Avatar on behalf of the user. This limitation persists even if the user has granted approval to the contract for spending their
ERC721 tokenId.

Mitigation
Modify the if statement to include support for approved operators as well:

// KeepersAvatarAssignment.sol

address owner = _ownerOf(tokenId);


bool allowed = (owner == msg.sender) || (getApproved(tokenId) == msg.sender) && (isApprovedForAll(owner, msg.sender));
if (!allowed) revert NoSpendingRights(msg.sender, tokenId);

[L-10] No Inidication That bulkAssignRandomAvatarConfigs Failed to Assign a


Specific TokenId
Affected Functions
KeepersAvatarAssignment

bulkAssignRandomAvatarConfigs

Description
After the closure of the minting window, the administrator will initiate the bulkAssignRandomAvatarConfigs function to convert
unconverted tickets into avatars. This function receives the parameters start and count , indicating the token IDs to be
converted. It then iterates through these token IDs, attempting to convert and assign random configurations to each one.
However, in certain instances, the assignment for a particular token may fail due to the fact that the traits bitmap is already in
use.

// KeepersAvatarAssignemtn.sol::assignRandomAvatarConfig

if (l.avatarConfigTraitsTaken[takenTraitsBitmap]) {
// this config is already taken
return false;
}

In the event of a failed assignment, the assignRandomAvatarConfig function returns a boolean value of false, and the
bulkAssignRandomAvatarConfigs function proceeds to the next iteration (tokenId) without providing any indication of the failed

conversion. This lack of feedback is highly inconvenient for the administrator as they are unaware of which tokenIds have
encountered failures. Consequently, this situation may result in unnecessary gas expensive calls to the
bulkAssignRandomAvatarConfigs function.

Mitigation
To ensure visibility and provide an indication of the token IDs that encounter failures during the configuration process, it is
recommended to emit an event whenever a token fails to be configured. By emitting an event, relevant information regarding

Keepers Findings Details 35


the failed token IDs can be captured, allowing for proper tracking and analysis of the failures.

Informational
[I-01] Gas Optimization: Struct Variable Tight Packing in
KeepersERC721Storage
Affected Functions

Affected Functions
KeepersERC721Storage

layout

Description
The  KeepersERC721Storage  contract employs several uint256 variables,
specifically  maxPerAddress ,  maxCommitmentBlocks ,  minCommitmentBlocks , and  _numAvailableTokens . These variables are currently
using an unnecessarily large data type, uint256, and could be efficiently packed into a single uint256 for gas optimization.

// KeepersERC721Storage.sol

struct Layout {
...
uint256 _numAvailableTokens;
uint256 minCommitmentBlocks;
uint256 maxCommitmentBlocks;
uint256 maxPerAddress;
...
}

In the current implementation, each of these four variables uses a separate slot in the storage, requiring more gas when
accessing or modifying these values. This inefficient use of storage could lead to higher costs when interacting with the
contract.

Mitigation
It’s recommended to tightly pack these variables into a single uint256. As these variables have a relatively small range, using
smaller data types such as uint8 and uint16 would be more gas-efficient without losing the ability to store the necessary
values.

Here is an example of how these variables could be packed:

// KeepersERC721Storage.sol

struct Layout {
...
uint16 _numAvailableTokens;
uint8 minCommitmentBlocks;
uint8 maxCommitmentBlocks;
uint16 maxPerAddress;
...
}

[I-02] Inconsistent Naming of Storage Variables


Affected Functions

Keepers Findings Details 36


KeepersERC721Storage

layout

TermsStorage

layout

Description
Generally, the naming convention for storage field names in the Keepers project does not employ underscores. However,
there are inconsistencies found in the
fields  _numAvailableTokens  and  _termsVersion  within  KeepersERC721Storage  and  TermsStorage , respectively. These underscore-
prefixed names deviate from the convention established in the rest of the project.

Mitigation
To resolve this, it is recommended to remove the leading underscores from the field names
in  KeepersERC721Storage  and  TermsStorage . Aligning these names with the convention used throughout the rest of the project
will provide greater consistency and improve readability.

[I-03] Inconsistent Naming of the KeepersERC721 Facet Contract


Affected Components
KeepersERC721  facet contract

Description
The majority of the facet contracts in the project use the  Facet  suffix in their naming convention, but
the  KeepersERC721  contract does not. This represents a minor inconsistency in the project’s naming convention.

Mitigation
To rectify this, it is recommended to append the  Facet  suffix to the  KeepersERC721  contract name. This change would align the
naming convention with the standard used throughout the rest of the project.

[I-04] Redefinition of Multiple Constants


Affected Components
KeepersERC721

ERC721r

RoomNamingFacet

TermsFacet

Description
Throughout the codebase, several constants are redefined multiple times, which may lead to potential confusion or
inconsistency. It is advisable to define these constants in a single location and import them wherever necessary.

The following constants are redefined in multiple contracts:

MAX_TICKETS

ERC721r

KeepersERC721

Keepers Findings Details 37


RoomNamingFacet

WHERE_TO_FIND_TERMS

KeepersERC721

TermsFacet

Mitigation
To address this issue, consider creating a constants library that stores all constants for the project. Alternatively, designate
one contract to hold a specific constant (for instance, the  WHERE_TO_FIND_TERMS  could be exclusively defined in
the  TermsFacet  contract).

In either scenario, ensure each constant is defined once and imported where needed.

[I-05] Usage of Multiple Magic Numbers and Strings


Affected Functions
KeepersDiamond

constructor

RoomNamingFacet

createSpecialTickets

setRoomName

getRoomName

Description
Throughout the codebase, several magic numbers and strings are utilized. For some of these, constants already exist but
are not employed.
The following code snippets illustrate the usage of magic numbers and strings:

// KeepersDiamond.sol
_grantRole(keccak256("KEEPERS_TERMS_OPERATOR"), msg.sender);
_grantRole(keccak256("LICENSE_OPERATOR"), msg.sender);
...
KeepersERC721Storage.layout()._numAvailableTokens = 20000;

// RoomNamingFacet.sol
function createSpecialTickets() external payable onlyOwner {
uint256 nonce;
RoomNamingStorage.Layout storage l = RoomNamingStorage.layout();
while (l._specialTicketsCount.current() < 10) { // Magic number
uint256 randomTicketId = PseudoRandomLib.getPseudoRandomNumber(++nonce) % MAX_TICKETS;
if (l.roomNamingRights[randomTicketId] == 0) {
// Then assign a random room
l._specialTicketsCount.increment();
l.roomNamingRights[randomTicketId] = uint8(l._specialTicketsCount.current());
}
}
emit SpecialTicketsRevealed();
}
...
function setRoomName(uint256 tokenId, uint256 roomId, string memory name) public {
...

// validate the room id is valid


if (roomId < 1 || roomId > 10) { // Magic number
revert InvalidRoomId(roomId);

Keepers Findings Details 38


}
...
}
...
function getRoomName(uint256 roomId) external view returns (string memory) {
RoomNamingStorage.Layout storage l = RoomNamingStorage.layout();

if (roomId < 1 || roomId > 10) { // Magic number


revert InvalidRoomId(roomId);
}
...
}

Mitigation
Consider using named constants instead of magic numbers and strings.
Here are some examples of how constants can be employed:

// KeepersDiamond.sol
_grantRole(TermsFacet.TERMS_OPERATOR, msg.sender);
_grantRole(LicenseFacet.LICENSE_OPERATOR, msg.sender);
...
KeepersERC721Storage.layout().numAvailableTokens = KeepersERC721.MAX_TICKETS;

// RoomNamingFacet.sol
uint8 constant MAX_SPECIAL_ROOMS = 10;
uint8 constant UNCHANGED_ROOM = 0;
...
function createSpecialTickets() external payable onlyOwner {
uint256 nonce;
RoomNamingStorage.Layout storage l = RoomNamingStorage.layout();
while (l._specialTicketsCount.current() < MAX_SPECIAL_ROOMS) {
uint256 randomTicketId = PseudoRandomLib.getPseudoRandomNumber(++nonce) % MAX_TICKETS;
if (l.roomNamingRights[randomTicketId] == UNCHANGED_ROOM) {
// Then assign a random room
l._specialTicketsCount.increment();
l.roomNamingRights[randomTicketId] = uint8(l._specialTicketsCount.current());
}
}
emit SpecialTicketsRevealed();
}
...
function setRoomName(uint256 tokenId, uint256 roomId, string memory name) public {
...
// validate the room id is valid
if (roomId == UNCH

ANGED_ROOM || roomId > MAX_SPECIAL_ROOMS) {


revert InvalidRoomId(roomId);
}
...
}
...
function getRoomName(uint256 roomId) external view returns (string memory) {
RoomNamingStorage.Layout storage l = RoomNamingStorage.layout();

if (roomId == UNCHANGED_ROOM || roomId > MAX_SPECIAL_ROOMS) {


revert InvalidRoomId(roomId);
}
...
}

[I-06] Unused Base-Contract AccessControlInternal in KeepersAdminFacet

Affected Functions

Keepers Findings Details 39


KeepersAdminFacet

Description
The  KeepersAdminFacet  contract inherits from the  AccessControlInternal  contract, but it does not make use of any of its
functions. This redundancy leads to an unnecessary waste of gas and should be addressed.
The  KeepersAdminFacet  contract should be modified to inherit from  AccessControl  instead of  AccessControlInternal .

[I-07] Remove Unused Imports


Description
The provided files contain unused contract imports, which can result in unnecessary gas consumption during deployment,
and and poor code readability. Below is an exhaustive compilation of all the unused imports, identified throughout the entire
codebase:

ERC721r.sol (L5) -> import "@openzeppelin/contracts/utils/Address.sol"; (Remove usage as well)


KeepersERC721.sol (L4) -> import "@openzeppelin/contracts/utils/Counters.sol";
KeepersERC721.sol (L5) -> import "operator-filter-registry/src/upgradeable/RevokableDefaultOperatorFiltererUpgradeable.sol";
KeepersAvatarAssignment.sol (L12) -> import "hardhat/console.sol";
KeepersERC721Metadata.sol (L10) -> import { KeepersERC721Storage } from "./KeepersERC721Storage.sol";

[I-08] Missed Opportunity for Gas Saving When Burning Tokens Due
to  _beforeTokenTransfer  Override in  KeepersERC721
Affected Functions
KeepersERC721

_beforeTokenTransfer(address from, address to, uint256 tokenId)

Description
The  _beforeTokenTransfer  function in  KeepersERC721  overrides the  _beforeTokenTransfer  function in  ERC721Base :

/**
* @notice ERC721 hook: clear per-token URI data on burn
* @inheritdoc ERC721BaseInternal
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId);

if (to == address(0)) {
delete ERC721MetadataStorage.layout().tokenURIs[tokenId];
}
}

This prevents the  ERC721MetadataInternal  function from being called, which means that the gas savings for burning tokens is
not applied.
In the  _beforeTokenTransfer  function in  KeepersERC721 , invoke the parent’s  _beforeTokenTransfer  function.

[I-09] Diamond Facet Upgrade

Keepers Findings Details 40


Affected Functions
KeepersDiamond

diamondCut

Description
If during an upgrade diamondCut() calls are executed in multiple Ethereum transactions, users may be exposed to contracts
that are upgraded only partially, i.e., some of the
functions are upgraded while others are not. This may result in unexpected inconsistencies.
We recommend upgrading the contracts in a single transaction, or making the fallback function pausable for the duration of
an upgrade.

[I-10] Consider Using Always Explicitly uint256 Instead of uint

Affected Contracts
ERC721r

L175

for (uint i = 0; i < pendingCommit.numNfts; i++) {

L212

function getAvailableTokenAtIndex(uint256 indexToUse, uint updatedNumAvailableTokens) internal returns (uint256) {

L274

function _mintAtIndex(address to, uint index) internal virtual {

L255

uint tokenId = getAvailableTokenAtIndex(index, l._numAvailableTokens);

KeepersERC721Metadata

L82

for (uint i; i < traitIdsLength; ) {

KeepersERC721Storage

L17

mapping(uint => uint) _availableTokens;

Description
Being inconsistent with variable data types could lead to the dev forgetting the data they’ve got to play with, this is also
crucial since the project is utilizing diamond proxies which uses delegateCall that calculates the functions sighash based on

Keepers Findings Details 41


the data types it receives.

Using explicit data types sizes could prevent future bugs.


Make sure you decalre variables as uint256 instead of uint .

[I-11] Gas Optimization: It is Not Necessary to Instantiate uint256 Variables to


Zero
Affected variables
TermsFacet

getTerms

uint256 i = 0

PseudoRandomLib

findRandomSetBitIndex

uint256 low = 0

ConfigLib

getTraitIdsFromConfig

uint256 i = 0

numTraitsInConfig

uint256 numTraits = 0

KeepersERC721

adminMintTickets

uint256 i = 0

ERC721r

reveal

uint256 i = 0

KeepersAvatarAssignment

createTraitsBulk

uint256 i = 0

Description
By default, the value of uint256 is zero, therefore, it is sufficient to declare it simply as uint256 i; . This approach not only
improves code readability but also helps save gas during execution. Specifically, gas can be saved each time the code is
executed.

[I-12] Gas Optimization: Use Named Return Variables in a Function Returns to


Save Gas
Affected functions
TermsFacet

getTerms

Keepers Findings Details 42


ConfigLib

configToTraitBitmap

ERC721r

_mintRandomTokenId

getAvailableTokenAtIndex

Description
Using a return named parameters instead of using the return statements can lead to significant gas savings. Since this
function is expected to be frequently used, implementing this change is of considerable importance.

To optimize the code, we propose the following adjustments:

1. Return named paramaeter: function _mintRandomTokenId(address to, uint256 randomNum) internal virtual returns (uint256

tokenId) , and remove return tokenId statement.

2. Return named paramaeter: function getAvailableTokenAtIndex(uint256 indexToUse, uint updatedNumAvailableTokens) internal

returns (uint256 result) , and remove return result statement.

3. Return named paramaeter: function configToTraitBitmap(uint256 config) internal pure returns (uint256 traitBitmap) , and
remove return traitBitmap statement.

4. Return named paramaeter: function getTerms() external view returns (string memory terms) , and remove return terms

statement.

[I-13] Gas Optimization: Use !=0 Instead of > 0 for uint Variables
Affected code
KeepersAvatarAssignment

getPriceForConfig

while (config > 0)

KeepersERC721Metadata

_tokenURI

if (r.roomNamingRights[tokenId] > 0)

TermsFacet

getTerms

while (bytes(l.termsParts[i]).length > 0)

Description
The operation GT(0x11) is more expensive than using a non-equal operator. And considering how many times this will be
executed, the amount of gas saved will be important.

[I-14] Extract Array’s Length to Local Variable Instead Iterating on While/For


Loops
Affected code
TermsFacet

getTerms

Keepers Findings Details 43


while (bytes(l.termsParts[i]).length > 0)

KeepersAvatarAssignment

createTraitsBulk

for (uint256 i = 0; i < traits.length; i++)

Description
The code can be optimized by minimising the number of SLOADs. SLOADs are expensive 100 gas compared to MLOADs.

Caching the array length outside a loop saves reading it on each iteration, as long as the array’s length is not changed
during the loop.
Consider changing the while loop in the getTerms function:

// TermsFacet.sol

uint256 termsPartsLength = bytes(l.termsParts[i]).length;


while (termsPartsLength > 0)

Consider changing the for loop in the createTraitsBulk function:

// KeepersAvatarAssignment.sol

uint256 traitsLength = traits.length


for (uint256 i = 0; i < traitsLength; i++)

[I-15] Gas Optimization: Use “unchecked” When Incrementing and


Decrementing uints
Affected code
Use the following cross-project search to find all the issue ( CMD + Shift + F )

“++ “

“-- “

Description
In certain sections of the code, there are instances where unsigned integers (uints) are incremented or decremented by a
value of 1 without utilizing an unchecked block.

To optimize gas usage, it’s recommended to incorporate unchecked blocks whenever the ++ or -- operators are employed.
By doing so, the built-in Solidity overflow/underflow checks can be bypassed. It is important to note that in these specific
cases, there is no possibility of encountering overflows or underflows, hence utilizing the unchecked block is safe and
efficient.

[I-16] Code Redability: Move Bitwise Operations to Functions


Affected Contact
KeepersAvatarAssignment

Description

Keepers Findings Details 44


The contract KeepersAvatarAssignment relies heavily on bitwise operations to extract and validate data from bitmap uint256
variables. These operations are utilized for various purposes, such as extracting user-supplied configuration parameters or
retrieving bitmap data stored on the blockchain.
A specific example is the extraction of traitId and categoryId from the bitmap. This operation is performed multiple times
across several functions within the contract.

// KeepersAvatarAssignment.sol

uint256 traitId = config & 0xFF;

// KeepersAvatarAssignment.sol

traitToCheck.categoryId != (config >> 8) & 0xF

To enhance code readability and minimize the potential for errors, we recommend to refactor the existing code by
introducing dedicated functions with descriptive names for performing these repetitive bitwise operations. This approach will
help to clearly indicate the purpose and intention of each operation. Here are some suggested function names:

1. extractTraitIdFromBitmap : This function can be used to extract the traitId from a given bitmap.

2. extractCategoryIdFromBitmap : This function can be employed to extract the categoryId from a provided bitmap.

By encapsulating these bitwise operations within dedicated functions, the code will become more readable, self-explanatory,
and less prone to mistakes.

function extractTraitIdFromBitmap(bitmap) external pure returns(uint8 traitId) {


uint8 traitId = config & 0xFF;
}

function extractCategoryIdFromBitmap(bitmap) external pure returns(uint8 categoryId) {


uint8 categoryId = (config >> 8) & 0xF;
}

[I-17] Inconsistency With uint size data types


Affected Contact
KeepersAvatarAssignment

KeepersAvatarAssignmentStorage

ConfigLib.sol

KeepersERC721Metadata

Description
The traitId variable represents a number within the range of 1-242, while the categoryId variable represents a number
within the range of 0-6.
Considering the limited range of values for both variables, it is sufficient to utilize uint8 as the data type, which can
accommodate values ranging from 1 to 255.

However, upon reviewing the mentioned contracts, it is evident that different uint sizes are being assigned to traitId and
categoryId in various places. Examples of this include assignments with data types ranging from uint16 to uint256 :

Keepers Findings Details 45


// KeepersAvatarAssignemntStorage.sol

struct Trait {
// the following variables will be
// tightly packed into 256 bits
uint32 id; // 1 - 242 (the trait id)
uint32 categoryId; // 0 - 6 (the category id)

struct Layout {
// mapping of trait id to trait
mapping(uint256 => Trait) traits;

// KeepersAvatarAssignemnt.sol

error InvalidTraitId(uint256); // uint8 should be enough

uint256 traitId = traits[i].id; // uint8 should be enough

uint256 traitId = config & 0xFF; // uint8 should be enough

function trait(uint256 traitId) // uint8 should be enough

uint256[7] memory categoryCounts; // uint256 is too much

if (traitToCheck.categoryId == uint16(Category.Body)) { // Can be casted to uint8

// ConfigLib.sol

function getTraitIdsFromConfig // traitIds are uint256


function getCategoryName // Casting categoryId to uint64
function packCategoryAndTraitIntoConfig // using uint256

// KeepersERC721Metadata.sol

uint256[] memory traitIds = ConfigLib.getTraitIdsFromConfig(bitmapConfig); // using uint256


uint256 traitIdsLength = traitIds.length; // uint8 is enough

The aforementioned examples are just a subset of instances where inconsistencies exist regarding the uint variable sizes for
traitId and categoryId . It is important to note that these inconsistencies extend beyond the provided examples.

To optimize gas usage and enhance code readability, it is strongly recommended to ensure consistency in the usage of uint
variable sizes throughout the codebase. Specifically, both traitId and categoryId should be declared as uint8 to align with
their expected value ranges. By maintaining this consistency, gas optimization can be achieved while also promoting
improved code clarity and maintainability.

[I-18] Change The Order of Functionalities to Save Gas


Affected Contact
KeepersAvatarAssignment

Description
Significant gas savings can be achieved by reordering the processes within the KeepersAvatarAssignment contract. Here are
some examples:

1. In the convertTicketToAvatar function, perform the check for takenTraitsBitmap before executing the more resource-
intensive functions such as getConfigValidity and getPriceForConfig .

Keepers Findings Details 46


2. Within the assignRandomAvatarConfig function, the if (l.avatarConfigTraitsTaken[takenTraitsBitmap]) condition can be
executed before the findWeightedRandomAvatarConfig function, which can be expensive.

3. In the findWeightedRandomAvatarConfig function, certain lines of code require reordering for optimal efficiency.

// KeepersAvatarAssignment.sol

uint256 bodyTraitId = findWeightedRandomBodyTrait(cumulativeValidTraitsBitmap, randNum);


if (isSpecialTicket) bodyTraitId = GLASS_BODY_ID;

can be changed to:

// KeepersAvatarAssignment.sol

uint256 bodyTraitId;
if(isSpecialTicket) {
bodyTraitId = GLASS_BODY_ID;
} else {
findWeightedRandomBodyTrait(cumulativeValidTraitsBitmap, randNum);
}

To prevent the execution of findWeightedRandomBodyTrait in case it’s a special ticket.

4. In the findWeightedRandomAvatarConfig it is not necessary to generate a new random number, since a new number is being
generated in every iteration in the for loop inside the bulkAssignRandomAvatarConfigs function.

Keepers Findings Details 47

You might also like