REVIEWED BY: Elmyre
Preface:
This is a complete review of the ChirpFinance solidity project based on the available code in:
- https://scan.mypinata.cloud/ipfs/bafybeignzhc77l2eszm4jvjvvnx2t2hy7lxdo4prnpnovzqqntsg47kmmy/#/address/0xCa66B54a8A4AD9a231DD70d3605D1ff6aE95d427?tab=contract
- https://scan.mypinata.cloud/ipfs/bafybeignzhc77l2eszm4jvjvvnx2t2hy7lxdo4prnpnovzqqntsg47kmmy/#/address/0x8d1e3458dA9E8a685732322D435178E790486651?tab=contract
- https://scan.mypinata.cloud/ipfs/bafybeignzhc77l2eszm4jvjvvnx2t2hy7lxdo4prnpnovzqqntsg47kmmy/#/address/0x5CCD0290a286c9ED47D0CabdECC802c5Cfc10310?tab=contract
- https://scan.mypinata.cloud/ipfs/bafybeignzhc77l2eszm4jvjvvnx2t2hy7lxdo4prnpnovzqqntsg47kmmy/#/address/0xB7b7b716E834556D72caA2Ce39D76dF46aeF5Af2?tab=contract
- https://scan.mypinata.cloud/ipfs/bafybeignzhc77l2eszm4jvjvvnx2t2hy7lxdo4prnpnovzqqntsg47kmmy/#/address/0xD233e8D42F721B0F2010C5CA337607749A0DE85b?tab=contract
The code is fairly complex so I would advise anyone that is reading this document to also look at other reviews and analysis of the contract! Some issues or security concerns might not be covered here due to my lack of knowledge and understanding, so getting a second opinion is a good thing to keep in mind! This review is based on the deployed version of the code.
Code Review: ChirpToken
The ChirpToken smart contract is for a token called ” Chirp.Finance” with governance features and functionalities for voting delegation. It implements a delegation mechanism where token holders can delegate their voting power to another address. This is commonly used in governance systems where token holders can vote on proposals, and they can also choose to delegate their voting power to a trusted address.
The contract maintains a history of voting checkpoints for each account (address), allowing for efficient vote retrieval and delegation updates.
It also inherits from a modified version of the OpenZeppelin’s ERC20.sol smart contract V3.3.0 (@OpenZeppelin/openzeppelin-contracts/blob/v3.4.2/contracts/token/ERC20/ERC20.sol).
The changes to the default OppenZepplin’s contract, and their impacts are as follows:
- Inherits from Onwable:
- This means that Chirp also inherits it although it doesn’t extend it directly. This provides basic access control mechanisms, where there is an account (an owner) that can be granted exclusive access to specific functions. By default, the owner account will be the deployment address (0x4C0B1DE367f9BcB07e524aD08F0FCf89c752186A). This can later be changed with the transferOwnership
- Addition of the “mint” function:
- This function simply calls the already existing “_mint” function but can only be called by the contract’s owner. This will enable to owner to mint however many tokens it wants to himself.
- Addition of the “_burnFrom” function:
- This function’s goal is to destroys a specified amount of tokens from a given address. This method seems to have been taken from the sol smart contract (also from OpenZepplin V3.3.0) but it has a bug where the _approve function is being called after the _burn one, which should not be the correct behaviour. Since this is an internal function tough, and is not called by the contract, this bug does not matter.
- Removal of the “virtual” keyword from multiple function:
- The functions from which this keyword was removed cannot be overridden anymore, meaning they will always behave as defined with this contract. This was done to the following functions: transfer, _transfer, allowance, approve, _approve, transferFrom, increaseAllowance, _mint and _burn.
Other than the mentioned changes, the ERC20.sol smart contract provides basic token functionality.
- Constants:
- DOMAIN_TYPEHASH: A constant representing the EIP-712 type hash for the contract’s domain.
- DELEGATION_TYPEHASH: A constant representing the EIP-712 type hash for the contract’s domain.
Note: DOMAIN_TYPEHASH and DELEGATION_TYPEHASH are both part of the EIP-712 standard and is used for structured data hashing. It is specifically designed for off-chain signature verification and is commonly employed in decentralized applications to enhance the security and efficiency of cryptographic signatures. Please check for reference https://eips.ethereum.org/EIPS/eip-712.
- Structs:
- Checkpoint: Struct used to record the number of votes for a specific block.
- fromBlock: The block where the votes were made.
- votes: the number of votes.
- State Variables and Mappings:
- _delegates: Mapping that keeps track of each address’s delegates.
- checkpoints: Mapping that stores a list of voting checkpoints by index for each address. Each checkpoint includes the block number and the number of votes at that block.
- numCheckpoints: Mapping that tracks the number of checkpoints for each address.
- nonces: Mapping that stores nonces for each address to prevent replay attacks.
- Events:
- DelegateChanged: Event emitted when an address changes its delegate.
- DelegateVotesChanged: Event emitted when a delegate addresses’ vote balance changes
- Public/External Functions:
- mint: Public function used to creates new tokens and sends them to the specified address and calls the _moveDelegates internal function. Can only be called by the contract’s owner (MasterChef).
- delegates: External view function that retrieves the delegate for a given address.
- delegate: External function the allows the caller (sender) to delegate their votes to another address via the _delegate function.
- delegateBySig: External function that implements EIP-712 signature verification to securely allow off-chain delegation of votes. The following steps are taken:
- Creating Domain Separator – The DOMAIN_TYPEHASH is used to create the type hash for the domain separator.
- Creating Delegation Struct Type Hash – The DELEGATION_TYPEHASH is used to create the type hash for the delegation struct.
- Creating Struct Hash – The function parameters (delegatee, nonce, expiry) are encoded and hashed to create a struct hash.
- Creating Digest – The domain separator and struct hash are concatenated and hashed to create the final hash.
- Signature Verification – The ecrecover function is used to recover the address that signed the digest. This recovered address is then compared to ensure it matches the expected signer.
This entire process ensures that the off-chain signature is associated with a specific domain and structure, enhancing the security of the delegation process in the contract.
- getCurrentVotes: External view function that returns the current vote balance for a given address.
- getPriorVotes: External view function that returns the vote balance for a given address at a specific block. It uses binary searching for optimizing performance and gas costs.
- Internal Functions:
- _delegate: Updates the delegate for a given address based on the provided values and moves votes accordingly. Emits the “DelegateChanged” event. Calls the _moveDelegate
- _moveDelegates: Moves votes between representatives (delegates) when the delegate changes.
- _writeCheckPoint: Writes a new checkpoint for a delegate, updating the vote balance at a specific block.
- Safe32: Pure function that ensures a given number can fit into a uint32 variable. Used by the _writeCheckpoint function to guarantee that the “number” does not exceed 32 bits.
- getChainId: Pure function that retrieves the chain ID.
The contract borrows and modifies code from YAM and COMPOUND protocols for governance, meaning that it has been adapted from existing audited code.
ChirpTimeLock
The ChirpTimeLock smart contract is designed to implement a time-locked queue for executing specific functions on a target contract.
This contract can be used to schedule and execute various administrative functions on the target contract with a time delay. The owner (by default the deployer) has the exclusive right to queue and execute transactions, ensuring controlled and time-locked governance.
The Constructor Initializes the targetContract (0x8d1e3458da9e8a685732322d435178e790486651 – the Nest Contract based on the deployed parameters) and sets the owner as the address that deployed the contract (0x4C0B1DE367f9BcB07e524aD08F0FCf89c752186A).
Note: An interesting resource about wallet addresses and smart contract addresses:
- https://medium.com/coinmonks/smart-contract-address-creation-method-difference-between-smart-contract-address-and-wallet-97b421506455
- Constants:
- TIME_DELAY: Constant representing the time delay for executing transactions, set to 43200 seconds (12 hours). The transactions cannot be executed before this delay as already passed.
- Error Definitions:
- NotOwnerError: Used to indicate that the caller is not the owner.
- AlreadyQueuedError: Indicates that a transaction with the given ID is already queued.
- NotQueuedError: Indicates that a transaction with the given ID is not queued.
- TimestampNotPassedError: Indicates that the current timestamp is before the specified execution timestamp.
- TxFailedError: Indicates that the transaction execution failed.
- Events:
- Queue: Emitted when a transaction is added to the queue.
- Execute: Emitted when a queued transaction is successfully executed.
- Cancel: Emitted when a queued transaction is cancelled.
- State Variables and Mappings:
- targetContract: The address of the target contract on which functions will be executed. Is set via the constructor and can never be changed (Should have been an immutable variable for efficiency).
- owner: The address that deployed the contract and has the authority to queue and execute transactions. Can not be changed.
- queued: A mapping to track whether a transaction with a given ID is queued.
- Modifiers:
- onlyOwner: A modifier that ensures that any function that uses it can only be called by the owner.
- Fallback Function:
- receive: External payable function that allows the contract to accept ETH. Since the function body it empty and no other function in the contract allow for ETH to be transferred out of the contract, whatever ETH is received will be forever lost (stuck inside the smart contract).
- Utility Function:
- getTxId: View function that generates the transaction ID for a received function using the keccak256 hash of target contract address, function name, function data, and execution timestamp. It also uses the encode(…) function to encode the function call input parameters into the specific binary format expected by the Ethereum Virtual Machine (EVM). This is essential for making function calls and interacting with other contracts on the Ethereum blockchain.
Note: For more information on the keccak256 function and abi.encode function check:
- https://docs.soliditylang.org/en/v0.8.23/internals/layout_in_storage.html#mappings-and-dynamic-arrays
- https://docs.soliditylang.org/en/v0.8.23/abi-spec.html
- Transaction Queueing and Execution Functions (only callable by the Owner):
- queueAdd: External function used to enqueue the add function of the targetContract. Generates a transaction ID and queues it, emitting the Queue event, if not already queued. Otherwise, reverts the transaction with the AlreadyQueuedError.
- queueSet: Same as queueAdd but for the set function of the targetContract.
- queueUpdateEmissions: Same as queueAdd but for the updateEmissionRate
- queueTransferOwnership: Same as queueAdd but for the transferOwnership
- queue: External generic function that allows the owner to enqueue any function call with custom data.
- execute: External function used to execute a queued transaction. It checks if the transaction is queued and the specified timestamp has passed, then executes the transaction on the target contract. It emits an Execute event upon successful execution. If the transaction is not queued, the minimum time not passed or the targetContract’s transaction call fails, it reverts with the NotQueuedError, TimestampNotPassedError or TxFailedError
- cancel: External function that allows the owner to cancel a queued transaction. It checks if the transaction is in the queue and emits a Cancel event. If the transaction is no in the queue, it reverts with the NotQueuedError.
Nest
The Nest smart contract is a decentralized finance (DeFi) protocol called that facilitates yield farming with the ChirpToken (referred to as cub). Users can deposit liquidity provider (LP) tokens into different pools and earn rewards in the form of cub tokens but need to pay a deposit fee. It also provides flexibility for the owner to add, modify, and update pools, as well as adjust the emission rate.
This smart contract inherits from two different versions of two OpenZeppelin:
- sol (@OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/access/Ownable.sol).
- sol (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/security/ReentrancyGuard.sol).
These provide basic access control mechanisms, where there is an account (an owner) that can be granted exclusive access to specific functions, as well as protection against re-entrancy attacks.
Note: For more information on re-entrancy attacks check:
- solidity-by-example.org/hacks/re-entrancy/
- quicknode.com/guides/ethereum-development/smart-contracts/a-broad-overview-of-reentrancy-attacks-in-solidity-contracts
- com/hack-solidity-reentrancy-attack
The contract also uses SafeMath for uint256 to prevent arithmetic overflows/underflows and SafeERC20 for safe token transfers (@OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/math/SafeMath.sol and @OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/token/ERC20/SafeERC20.sol)
- Structs:
- UserInfo:
- amount: The amount of LP tokens deposited by the user minus the deposit fee.
- rewardDebt: The amount of cub tokens that need to be subtracted from the user pending reward. This is needed because the user cannot be entitled to any of the accumulated cub tokens per share that were accrued up to the point where he deposit the LP tokens. The pending rewards for a user is based on the formula: “pending reward = user.amount x pool.accCubPerShare – user.rewardDebt”.
- PoolInfo:
- lpToken – Address of the LP token contract.
- allocPoint – Number of allocation point assigned to this pool (cubs to distribute per block).
- lastRewardBlock – Last block number were cubs distribution occurs.
- accCubsPerShare – Accumulated cubs per share, times 1012.
- depositFeeBP – Deposit fee in basis points. The maximum value is 10000 basis points (100%).
- Events:
- Deposit: Triggered when a user deposits LP tokens.
- Withdraw: Triggered when a user withdraws LP tokens.
- EmergencyWithdraw: Triggered when a user withdraws LP tokens via the emergencyWithdraw function, disregarding rewards.
- Constants:
- BONUS_MULTIPLIER: A constant bonus multiplier for early cub makers with value 1.
- State Variables and Mappings:
- cub: The ChirpToken contract.
- devaddr: The DEV address. Will receive a 10% of the minted cub on any pool update.
- feeAddress: The address where deposit fees are sent.
- cubPerBlock: The number of cub tokens created per block.
- poolInfo: An array of PoolInfo structs containing the information about each pool.
- userInfo: A mapping that keeps track of user-specific information for each pool (based on its index).
- totalAllocPoint: The total allocation points across all pools.
- startBlock: The block number when cub mining starts.
The Constructor initialized the cub, devaddr, feeAddress, cubPerBlock and startBlock when the contract is deployed, respectively, with the values 0xca66b54a8a4ad9a231dd70d3605d1ff6ae95d427 (Chirp.Finance token address), 0x4c0b1de367f9bcb07e524ad08f0fcf89c752186a, 0x4c0b1de367f9bcb07e524ad08f0fcf89c752186a (same for both and matches the owner of the ChirpTimeLock contract owner), 0 and 17694003.
- View and Helper Functions:
- poolLength: External view function that returns the total number of pools.
- getMultiplier: Public pure function that calculates the reward multiplier based on the block range.
- pendingCub: External view function that calculates the pending cub rewards for a user in a specific pool based on the pool index received and the user address. The returned value will the pending reward the user is entitled to, based on the formula described under
- Pool Management Functions (available only to the contract Onwer):
- add: Adds a new pool to the system and determines its deposit fee based on the received parameters. Guarantees that the fee is not higher than 10000 basis points (100%) and, based on a received flag, it can update all pools using the massUpdatePools
- set: Updates the allocation points and deposit fee for an existing pool based on the received parameters. Like add, enforces the maximum fee value, and can update all pools.
- updateEmissionRate: Updates the emission rate of cub tokens per block.
- User Interaction Functions:
- deposit: Allows users to deposit LP tokens in specific pool and updates it. If the user already has any deposited LP tokens in the pool, the rewards he accrued so far, if any, are also sent to him. If the pool has a deposit fee, the corresponding value is subtracted from the users LP token and sent to the feeAddress. Emits the Deposit event when successful.
- depositReferral: Same logic as deposit but includes and an additional referral address that will receive 25% of the deposit fees.
- withdraw: Allows users to withdraw LP tokens, updating it, and claim their pending rewards, if any. Guarantees that the user cannot withdraw more than its deposited amount minus the fees.
- emergencyWithdraw: Allows users to withdraw their LP tokens from a specific pool, forsaking their rewards and without the pool being updated. Emits the EmergencyWithdraw event.
- safeCubTransfer: Internal function used to simply clamp the amount of LP tokens to be transferred out of the contract, based on the total existing balance in the contract, in case of a rounding error.
- massUpdatePools: Updates the reward variables for all pools by calling the updatePool function for each existing pool (based on the poolInfo array).
- updatePool: Updates the reward variables for a specific pool if needed. It also mints the cub rewards to the DEV address and the contract itself (to later be distributed by the LP provider upon withdrawal).
- Owner Management Functions:
- dev: Updates the DEV address (devaddr). Can only be called by the current DEV address.
- setFeeAddress1: Updates the fee address (feeAddress). Can only be called by the current fee address.
Vertebrates
The Vertebrates smart contract is an implementation of an ERC-721A non-fungible token (NFT) contract with additional functionalities. It combines ERC-721A functionality with dynamic pricing, airdropping, configuration updates, and more. It’s designed for managing and minting vertebrate-themed NFTs. The owner has control over key parameters, and users can mint tokens by paying the appropriate amount of ether.
This smart contract inherits from other three contracts:
- sol (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/access/Ownable).
- sol (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/security/ReentrancyGuard.sol).
- ERC721A.sol (@chiru-labs/ERC721A/blob/v4.2.3/contracts/ERC721A.sol).
The first two provide basic access control mechanisms, where there is an account (an owner) that can be granted exclusive access to specific functions, as well as protection against re-entrancy attacks. ERC721A.sol is a NFT standard by chiru-labs that optimizes the minting of multiple NFTs.
The contract also uses the Strings (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/utils/Strings.sol) library to convert a uint256 to a string for concatenation and formatting purposes. This is often employed when generating dynamic data for metadata URIs or other string manipulations where integer values need to be represented as strings.
Note: For more information on this NFT standard implementation check:
- https://medium.com/@kccmeky/what-is-erc721a-93bffb0f1366
- https://www.infura.io/blog/post/comparing-nft-standards-erc-721-vs-erc-721a-vs-erc-1155
- https://github.com/chiru-labs/ERC721A
- https://www.alchemy.com/blog/erc721-vs-erc721a-batch-minting-nfts
The Constructor initializes the contract with the name “Vertebrates” and the symbol “VERT” and by default sets the owner as the contract deployer (0x4C0B1DE367f9BcB07e524aD08F0FCf89c752186A – matches the owner of the ChirpTimeLock contract and Nest ).
- State Variables and Mappings:
- uri: Stores the URI for token metadata
- uriSuffix: Suffix appended to the token ID when constructing the full URI for token metadata.
- hiddenMetadataUri: Represents a URI for hidden metadata (used when revealed is set to false).
- cost1: Define the minting costs for tokens when the total supply is less than 100.Default value is 750000 ether.
- cost2: Define the minting costs for tokens when the total supply is more than or equal to 100. Default value is 1000000 ether
- supplyLimit: Sets the maximum limit of total tokens that can be minted.
- maxMintAmountPerTx: Defines the maximum number of NFTs that can be minted per transaction.
- maxLimitPerWallet: Sets the maximum number of NFTs that a single wallet can mint.
- sale: Indicates whether the sale is currently active or paused.
- revealed: Determines whether metadata is revealed or hidden.
All these variables have specific set functions that only the owner can call, so they can be updated whenever the contract owner decides to. Will not discuss them in details since they simply change the value of the variables to whatever new values is received.
- Mint and Owner functions:
- UpdateCost: Internal view function that calculates the minting cost based on the current total token supply.
- Mint: Public payable function that allows users to mint a specified number of NFTs by paying the appropriate amount of ether, up to maxMintAmountPerTx. Checks various conditions, including the sale status, mint amount limits, and funds sufficiency.
- Airdrop: Public onlyOwner function that allows the owner to airdrop NFTs to a specified address. The only imposed limit is on the max supply of the NFTs and can be done at no cost (other that the normal gas cost for the transaction and the minting).
- withdraw: Public onlyOwner function that allows the owner to withdraw the contract’s balance.
- Read functions:
- price: Public View function that returns the current minting price based on the total token supply.
- tokensOfOwner: External View function that returns an array of token IDs owned by a specific address.
- _startTokenId: Internal view virtual override function that defines the starting token ID as 1.
- tokenURI: Public View virtual override function that returns the URI for a specific token based on whether metadata is revealed or hidden.
- _baseURI: Internal view virtual override function that returns the base URI for token metadata.
WrappedVertebrates
The WrappedVertebrates solidity smart contract is designed to wrap and unwrap ERC-721 (NFT) tokens into ERC-20 tokens. This allows users to convert non-fungible tokens (NFTs) into fungible tokens and vice versa.
This contract relies on two OpenZeppelin contracts:
- Inherits from ERC20.sol (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/token/ERC20/ERC20.sol).
- Depends on IERC721.sol (@OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/token/ERC721/IERC721.sol).
The Constructor initializes the contract with the address of the ERC-721 contract passed as an argument. On deployment, the Vertebrates‘s address was passed (0xb7b7b716e834556d72caa2ce39d76df46aef5af2).
- State Variables and Mappings:
- nftContract: The Vertebrates’ smart contract and the one used in the wrapNFT and unwrapNFT
- tokensPerNFT: This variable determines how many WrappedVertebrates tokens will be minted for each wrapped NFT. Is set as 1018 which is corresponds to the default ERC-20 decimal places.
- idToOwner: A mapping that keeps track of the owner of each wrapped NFT token.
- Wrap and Unwrap External Functions:
- wrapNFT: This function allows users to wrap their NFTs into ERC-20 tokens based on the received token IDs. It transfers NFTs from the sender to the WrappedVertebrates contract and mints a corresponding amount of WrappedVertebrates tokens and assigns them to the sender.
- unwrapNFT: This function allows users to unwrap their WrappedVertebrates tokens back into NFTs. It burns the specified amount of ERC-20 tokens from the sender and then transfers the corresponding Vertebrates NFTs back to the sender.
Conclusion & Security Concerns:
The ChipToken, ChirpTimeLock and Nest smart contracts are intertwined. At deployment, they were basically all controlled via the owner functions by the same address (0x4C0B1DE367f9BcB07e524aD08F0FCf89c752186A).
The ownership of the ChirpToken was already changed to the Nest contract (transferOwnership Transaction hash – 0xdd9a521cc715aa2af7740a6cc9bf32eb6951b0f2d51900570ccea3b9b0f8f7d2) after some tokens were minted by the previous owner. This means that, at this point, the ChirpToken is simply a rewards token for the Nest contract and is minted based on the rules defined in its functions.
The Vertebrates contract also as the same owner, and together with the WrappedVertebrates they allow users to mint NFTs and wrap and unwrap them based on the parameters defined by the owner.
Not considering the economic aspect of the different contracts and the possible gain or loss of value, the contract seems to have no bugs or security risk and the code looks to be functioning as intended.
The contract already has a security audit that is publicly available in https://github.com/bcaservice/audit-report/blob/main/ChirpFinance/Nest.pdf.