Professional Documents
Culture Documents
for
Keepers
Contact 3
About Revelator 4
Methodology 5
Severity Definition 5
Disclaimers 6
Executive Summary 7
Scope 8
Key Findings 9
Findings Details 10
Client Keepers
Website https://keepers.xyz/
Author Jonathan S
Classification Public
Contact
For more information please contact Ginger Security Inc.: hello@gingersec.xyz
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.
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.
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.
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.
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
contracts/facets/Terms/TermsStorage.sol 16
contracts/facets/License/LicenseStorage.sol 13
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
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
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
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
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
//ERC721r.sol
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:
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.
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
diamondCut
Description
The documentation specifies that KeepersDiamond should renounce its upgradability at some stage:
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;
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.
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 _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.
_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:
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:
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.
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
Applying this modifier to the reveal and convertTicketToAvatar functions introduces some challenges to the minting process
and overall user experience.
Consider the following scenario:
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.
getRandomWeightedRarityBitmap
getRandomTraitCount
Description
The vulnerability originates from the getRandomWeightedRarityBitmap function, where the assignment of avatar configurations
with different rarity traits occurs.
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.
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:
2. The first 10 generated random numbers ( rand ) for assigning traits are as follows:
Rand 2: 18 (Rare)
Rand 3: 22 (Uncommon)
Rand 6: 8 (Rare)
Rand 7: 91 (Uncommon)
3. The assigned rarity distribution for the configurations, based on these rand values, is as follows for just the first 10
tokenIds:
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 :
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
...
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
...
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.
reveal
Description
pendingCommit.commitBlock + l.minCommitmentBlocks;
Subsequently, the 13th element from the block header is extracted and labelled as randao .
// ERC721r.sol
// ERC721r.sol
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
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
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 = {}
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
This strategy is also used in the CryptoPunks project to generate random ids.
// CryptoPunksV2
[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
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 :
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 .
constructor
Description
In KeepersDiamond.sol , the maxPerAddress field of KeepersERC721Storage is initialized with a value of 10:
// KeepersDiamond.sol
KeepersERC721Storage.layout().maxPerAddress = 10;
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
// ERC721r.sol
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
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.
// 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
ERC721r
reveal
KeepersERC721
adminMintTickets
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.
_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
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
withdraw
Description
The withdraw function in the KeepersAdminFacet contract uses transfer to send ETH:
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:
KEEPERS_TERMS_OPERATOR role
LICENSE_OPERATOR role
Given the sensitive nature of these functionalities, a compromise of this address would jeopardize the entire system's
security.
[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
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.
diamondCut
KeepersAvatarAssignment
CreateTraitsBulk
bulkAssignRandomAvatarConfigs
ERC721Base
KeepersERC721
adminMintTickets
LicenseFacet
setCommercialRightsOperator
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.
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.
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:
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.
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:
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:
[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
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
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
[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
While there are existing setters inside KeepersAdminFacet.sol , there is no default value added on the KeepersDiamond
// KeepersDiamond.sol
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.
_tokenURI
Description
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
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/ .
_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
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.
reveal
Description
The reveal function employs the subsequent code block to identify the id it will mint.
// ERC721r.sol
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
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.
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):
function findWeightedRandomBodyTrait(
uint256 cumulativeValidTraitsBitmap,
uint256 randSeed
) internal pure returns (uint256) {
uint256 randNum = PseudoRandomLib.deriveNewRandomNumber(randSeed);
uint256 rarityBitmap = getRandomWeightedRarityBitmap(randNum);
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:
Low
[L-01] No Checks on Start and Completion Time in Mint Window
Affected Functions
KeepersERC721
adminMintTickets(uint256 _count)
ERC721r
commit(uint128 numNFTs)
KeepersMintWindowModifiers
_isMintingWindowOpen()
whenMintWindowOpen()
whenMintWindowClosed()
KeepersAdminFacet
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
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
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
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)); .
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
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
_mintRandomTokenId
Description
There are functions within the system that do not adhere to the CEI (Check-Effect-Interactions) Pattern:
// ERC721r.sol
function reveal(
bool agreeToTermsOfService,
bytes calldata rlpEncodedEntropyBlockHeader
) external payable nonReentrant whenMintWindowOpen whenNotPaused {
KeepersERC721Storage.Layout storage l = KeepersERC721Storage.layout();
if (!agreeToTermsOfService) {
revert MustAgreeToTermsAndConditions();
}
// ERC721r.sol
// @audit Use safemint? even though they limit only EOA accounts?
_mint(to, tokenId);
--updatedNumAvailableTokens;
l._numAvailableTokens = updatedNumAvailableTokens;
return tokenId;
}
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.
Mitigation
Follow the CEI pattern in the mentioned functions above.
Description
The smart contracts employed in the project utilize floating pragmas, for example:
// KeepersERC721Storage.sol
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
// KeepersERC721Storage.sol
// KeepersAdminFacet.sol
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.
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.
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.
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 .
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.
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
// KeepersAvatarAssignment.sol
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
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
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.
// KeepersERC721Storage.sol
struct Layout {
...
uint16 _numAvailableTokens;
uint8 minCommitmentBlocks;
uint8 maxCommitmentBlocks;
uint16 maxPerAddress;
...
}
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.
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.
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.
MAX_TICKETS
ERC721r
KeepersERC721
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.
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 {
...
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
Affected Functions
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-08] Missed Opportunity for Gas Saving When Burning Tokens Due
to _beforeTokenTransfer Override in KeepersERC721
Affected Functions
KeepersERC721
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.
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.
Affected Contracts
ERC721r
L175
L212
L274
L255
KeepersERC721Metadata
L82
KeepersERC721Storage
L17
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
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.
getTerms
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.
1. Return named paramaeter: function _mintRandomTokenId(address to, uint256 randomNum) internal virtual returns (uint256
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
KeepersERC721Metadata
_tokenURI
if (r.roomNamingRights[tokenId] > 0)
TermsFacet
getTerms
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.
getTerms
KeepersAvatarAssignment
createTraitsBulk
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
// KeepersAvatarAssignment.sol
“++ “
“-- “
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.
Description
// KeepersAvatarAssignment.sol
// KeepersAvatarAssignment.sol
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.
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 :
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
// ConfigLib.sol
// KeepersERC721Metadata.sol
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.
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 .
3. In the findWeightedRandomAvatarConfig function, certain lines of code require reordering for optimal efficiency.
// KeepersAvatarAssignment.sol
// KeepersAvatarAssignment.sol
uint256 bodyTraitId;
if(isSpecialTicket) {
bodyTraitId = GLASS_BODY_ID;
} else {
findWeightedRandomBodyTrait(cumulativeValidTraitsBitmap, randNum);
}
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.