REVIEWED BY: Elmyre
Preface
This is a complete review of the B9RewardsDistributorPct smart contract whose code can be found at https://scan.mypinata.cloud/ipfs/bafybeidn64pd2u525lmoipjl4nh3ooa2imd7huionjsdepdsphl5slfowy/#/address/0x40b5D3abCe7e49aec639d0c19F77FAb491430714?tab=contract.
Although the code is not overly complex, I would advise anyone who is reading this document to also look at other reviews and analyses of the contract! Some issues or security concerns might not be covered here due to my lack of knowledge or understanding of the contract. Getting a second opinion is always a good thing to keep in mind!
Overview
This smart contract is designed to distribute rewards, in the form B9 tokens, to eligible users, based on a provided Merkle Root. (B9 contract – 0xE676a1E969Feaef164198496bd787e0269f7b237).
The amount of claimable tokens is based on the amount of B9 tokens addresses held at the time of the snapshot. They can be claimed during a 14-day period, but the claimable amount decays linearly daily.
After each claim phase ends, the contract owner can burn all unclaimed tokens and start a new claiming phase, either with the same parameters or new ones. The owner must also deposit enough B9 in the contract before each claiming phase so users can successfully claim them.
As it stands, the contract can be used for multiple claiming phases (65,536 to be exact), and the code has been optimized to be as gas-efficient as possible.
The contract also features functions that allow the contract owner to withdraw from the contract any ETH or ERC20 tokens sent to it by mistake, making it so no funds will ever be mistakenly lost by being sent directly to the contract’s address.
On the other hand, any B9 tokens that are sent to the contract cannot be withdrawn and will be burned every time the burnRemainingTokens function is run.
Code Review
The B9RewardsDistributorPct smart contract manages a reward system where users can claim a percentage of B9 tokens based on their holdings, verified through a Merkle proof. It includes functions for managing the claim phases, token deposits, and withdrawals, along with safeguards and error handling.
The contract is designed to be owner-controlled with the ability to transition ownership, update claim parameters when creating new claiming phases, and handle unexpected token deposits.
- Dependencies:
- IERC20: @openzeppelin/contracts/token/ERC20/IERC20.sol – Standard interface implemented by all ERC-20 tokens. Allows the contract to call functions shared by all ERC-20 tokens, namely the balanceOf, transferFrom, and transfer functions
- IERC20WithBurn: Interface created to simply add the burn function to IERC20 since the B9 token implements ERC20Burnable, hence we know it supports it.
- MerkleProof: (@openzeppelin/contracts/utils/cryptography/MerkleProof.sol) – Library used for the verification of Merkle Tree proofs. Allows the verify function from this library to be used to confirm that the users are eligible to claim.
These are well-known and widely used for smart contract development, which reduces the risk of common vulnerabilities and security concerns.
- Constants:
- DECIMAL_MULTIPLIER: Number of decimal places = 1018.
- PERCENT_MULTIPLIER: Number of decimal places divided by 100 for percentage calculations = 1016.
- MAX_CLAIM_DAYS: The maximum claim days = 14.
- B9_TOKEN_ADDRESS: The B9 token address (different for Test vs Main Blockchain).
- Private State Variables:
- s_owner:
- The contract owner, initially the deployer.
- s_claimVersion:
- Tracks the current claim version. Starts at 0 and can go up to 65535.
- s_claimStartDate:
- The Start Date for the current claim phase.
- s_claimPercentage:
- Percentage of claimable B9 tokens based on the user’s snapshotted amount of tokens held.
- Starts at 69% for the first claim phase and can be updated for the following claim phases.
- s_B9TokenBalance:
- Balance of B9 tokens held by the contract that is deposited via the depositTokens
- Tokens sent directly to the contract’s address are not accounted for here.
- s_claimMerkleRoot:
- Merkle root hash for claim verification. It will be updated on every new claim phase.
- s_isClaimed:
- Mapping which tracks an address’ claim status for each claiming phase.
All state variables (except the mapping) are private for gas efficiency and the contract provides getters for all of them. When possible, the contract packs storage variables in the same memory slot to be more efficient when updating them.
- Custom Errors:
- NotOwner:
- Thrown when the caller is not the owner of the contract for onlyOwner
- ZeroAddressCannotBeOwner:
- Thrown in the transferOwnership function to prevent transferring ownership to the zero address.
- ClaimPhaseOngoing:
- Thrown in functions that should only be executed after the claim phase ends, such as burnRemainingTokens and createNewClaim, to ensure they are not called prematurely.
- ClaimPhaseNotStarted:
- Thrown in the _claim function when users try to claim tokens before the claim phase begins.
- ClaimPhaseEnded:
- Thrown in the _claim function when users try claiming tokens after the claim phase is over.
- AmountMustBePositive:
- Thrown in the depositTokens function when the amount of tokens being deposited is not positive.
- DepositB9TokensFailed:
- Thrown in the depositTokens function to signal a failure in transferring B9 tokens from the user to the contract.
- InvalidClaimPercentage:
- Thrown in the createNewClaim function when the claim percentage is not within the valid range (1 to 100).
- InsufficientFundsInTheContract:
- Thrown in various functions like _claim, withdrawAllEthTo, and withdrawTokensTo, when there are insufficient funds in the contract for the desired operation.
- AlreadyClaimed:
- Thrown in the _claim function when a user tries to claim more than once per claim phase.
- InvalidClaimData:
- Thrown in the _claim function when the provided claim data is invalid or does not match the Merkle proof.
- FailedToTranferClaimedTokens:
- Thrown in the _claim function to handle the case where the transfer of B9 tokens to the claimer fails.
- WithdrawEthFailed:
- Thrown in the withdrawAllEthTo function to handle failures in sending ETH to the specified recipient.
- WithdrawTokenFailed:
- Thrown in the withdrawTokensTo function to handle failures in transferring ERC20 tokens to the specified recipient.
- B9TokensCannotBeWithdrawn:
- Thrown in the withdrawTokensTo function when trying to withdraw B9 tokens from the contract.
The custom errors in the contract provide descriptive and gas-efficient ways to handle exceptional conditions.
- Events:
- B9TokensDeposited:
- Emitted when the owner deposits B9 tokens in the contract.
- B9TokensBurned:
- Emitted when the owner burn all existing B9 tokens in the contract.
- B9TokensClaimed:
- Emitted when a user successfully claims tokens.
- ETHWithdrawn:
- Emitted when the owner withdraws ETH mistakenly sent to the contract.
- TokenWithdrawn:
- Emitted when the owner withdraws ERC20 tokens (except B9) mistakenly sent to contract.
The Constructor, on deployment, initializes the contract, setting the contract’s owner as the deployer and starting the first claiming phase with a given Merkle Root.
- Modifers:
- onlyOwner:
- Restricts function access to the contract owner.
- claimPhaseOver:
- Restrict function access to after the claim phase has ended.
- Owner Only Functions:
- transferOwnership:
- Transfers contract ownership to a new owner.
- depositTokens:
- Allows the owner to deposit B9 tokens from his wallet into the contract.
- Deposited amount must be positive.
- The owner must have previously approved the B9 tokens spending.
- burnRemainingTokens:
- Allows the owner to burn all remaining B9 tokens in the contract after a claim phase ends.
- This will also burn B9 tokens directly sent to the contract (not deposited)
- createNewClaim:
- Allows the owner to initiate a new claim phase with updated parameters.
- Guarantees the received claimPercentage is within valid intervals.
- Only possible after the current claim phase is over.
- withdrawAllEthTo:
- Allows the owner to withdraw ETH mistakenly sent to the contract to any address.
- withdrawTokensTo:
- Enables the owner to withdraw any ERC20 mistakenly sent tokens sent to the contract, except B9 tokens, to any address.
- Uses the _checkReceivedTokenBalance to check the balance of the specified ERC20.
- External Functions:
- claim:
- Allows users to claim their rewards using a Merkle proof to verify the provided data.
- Uses the _claim function for the actual logic.
- claimAmount:
- View function that calculates the claimable amount tokens based on the current claim day.
- Uses the _claimAmout function for the actual calculation.
- claimDay:
- View function that returns the current claim day for the ongoing claim version.
- checkReceivedTokenBalance:
- View functions that exposes the _checkErc20TokenBalance function to check the balance available for withdrawal of a given ERC20 token.
- Internal Functions:
- _claim:
- Handles the distribution of claimable tokens.
- Responsible for all the needed verifications, including the Merkle Proof validation via the _verify function, reverting the transaction if necessary with the correct error.
- Updates s_B9TokenBalance and s_isClaimed based on the when the claim is successful.
- _today:
- Returns the current day based on the block timestamp.
- Will always return the current’s day timestamp at 00:00.
- _verify:
- Verifies a Merkle Proof based on the received parameters. Returns true if valid.
- _claimAmount:
- Calculates the amount of B9 tokens to be claimed based on the current claim day.
- The formula is equivalent to: UserAmount x ClaimPercentage * (1 – DaysSinceStart / 14);
- _checkReceivedTokenBalance:
- Checks the contract’s balance for a given ERC20 token.
Conclusion
This contract aims to be a simple way for B9 tokens to be distributed to users, based on a predetermined set of eligible addresses, via a claiming function. The way the contract checks eligibility by using a Merkle Root is very common, and it uses well-known and widely used code.
Also, although the contract has an Owner, this poses no risk whatsoever to the end user and those who run the claim. The permissions he has simply allow him to manage its own tokens and decide when and how he will allow the broader community to get them!
No bugs or other risks were found, and the codes seems to function as intended.