-
Notifications
You must be signed in to change notification settings - Fork 25
Using ERC20Burnable instead of ERC20 #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThe Changes
Sequence Diagram(s)sequenceDiagram
participant CATFactory
participant ContributionAccountingToken
participant Minter
CATFactory->>ContributionAccountingToken: createCAT(..., factoryAddress)
Note right of ContributionAccountingToken: Token stores factory address
ContributionAccountingToken->>CATFactory: onMinterRoleGranted(minter)
CATFactory->>CATFactory: update mintableTokens and isMinterForCAT mappings
Minter->>ContributionAccountingToken: mint(amount)
Note right of ContributionAccountingToken: Deduct 0.5% fee to treasury, mint net amount to minter
Poem
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (1)
contracts/CATFactory.sol (1)
80-87: Verify array bounds and consider gas optimization.The current implementation creates a new array and copies elements, which is correct but could be optimized for gas usage in some scenarios.
For better gas efficiency when returning large arrays, consider using assembly or a more direct approach:
function getCATAddresses(address _creator, uint256 start, uint256 end) external view returns (address[] memory) { address[] storage creatorTokens = administerableTokens[_creator]; if (creatorTokens.length == 0) { return new address[](0); } require(start <= end, "Start index must be less than or equal to end index"); require(start < creatorTokens.length, "Start index out of bounds"); if (end > creatorTokens.length) { end = creatorTokens.length; } uint256 resultLength = end - start; address[] memory result = new address[](resultLength); for (uint256 i = 0; i < resultLength; i++) { result[i] = creatorTokens[start + i]; } return result; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/CATFactory.sol(1 hunks)contracts/ContributionAccountingToken.sol(1 hunks)
🔇 Additional comments (5)
contracts/ContributionAccountingToken.sol (5)
5-5: LGTM! Import addition is correct.The import for
ERC20Burnableis properly added and necessary for the inheritance change.
9-9:Details
❓ Verification inconclusive
Consider access control implications for burn functionality.
The inheritance change from
ERC20toERC20Burnableis technically correct and addsburn(uint256)andburnFrom(address, uint256)functions. However, consider whether unrestricted token burning aligns with your tokenomics model.Benefits:
- Token holders can burn their own tokens
- Burning reduces total supply, potentially allowing more minting via
maxMintableAmount()Security considerations:
- Any token holder can burn their tokens without restrictions
burnFromallows burning from other accounts with sufficient allowanceVerify that unrestricted burning is intended for your token model:
🏁 Script executed:
#!/bin/bash # Description: Check if there are any existing burn-related functions or access controls # Search for any existing burn functionality or references rg -A 3 -B 3 "burn" --type sol # Check for any access control patterns that might need to be applied to burn functions rg -A 5 "onlyRole\|onlyOwner" --type solLength of output: 129
🏁 Script executed:
#!/bin/bash set -eu # Locate the ContributionAccountingToken contract file FILE=$(rg -l "contract ContributionAccountingToken" --glob '*.sol') echo "Found contract file: $FILE" # Check for any overrides or explicit access control on burn functions rg -n "function burn" "$FILE" || echo "No override for burn()" rg -n "burnFrom" "$FILE" || echo "No override for burnFrom()" rg -n "onlyRole" "$FILE" || echo "No onlyRole references" rg -n "onlyOwner" "$FILE" || echo "No onlyOwner references"Length of output: 1317
Review: Confirm burn functions access control
We’ve verified that ContributionAccountingToken inherits unrestricted
burn(uint256)andburnFrom(address,uint256)from ERC20Burnable, with noonlyRoleoronlyOwnerguards:• File:
contracts/ContributionAccountingToken.sol
• No overrides or access controls onburn/burnFromIf unrestricted token burning aligns with your tokenomics, no changes are needed. Otherwise, consider overriding these functions with appropriate access checks, for example:
function burn(uint256 amount) public onlyRole(MINTER_ROLE) { super.burn(amount); } function burnFrom(address account, uint256 amount) public onlyRole(MINTER_ROLE) { super.burnFrom(account, amount); }Please confirm that unrestricted burning is intentional.
5-5: LGTM: Import addition is correct.The import for ERC20Burnable is properly added to support the inheritance change.
5-5: LGTM! Import added correctly.The import for
ERC20Burnableis properly added and follows the existing import pattern.
9-9:Details
❓ Verification inconclusive
Verify burn functionality compatibility with tokenomics.
The addition of
ERC20Burnableenables token holders to burn their tokens, which will reduce the total supply. This change appears well-considered given the complex tokenomics, but please ensure:
- The
maxMintableAmount()calculation correctly handles reduced supply from burns- The economic model accounts for deflationary pressure from burning
- Transfer restrictions don't interfere with burn operations
Let me verify how burning interacts with the minting logic:
🏁 Script executed:
#!/bin/bash # Description: Check if burn functionality is used elsewhere and verify mint calculations # Expected: Burn should work correctly with existing mint logic since it reduces totalSupply() # Search for any existing burn functionality or references rg -A 5 -B 5 "burn|Burn" # Check if totalSupply() is used in calculations (it should handle burns correctly) rg -A 3 -B 3 "totalSupply"Length of output: 4529
Burn functionality verified against mint logic
- contracts/ContributionAccountingToken.sol › maxMintableAmount(): uses totalSupply() (which decreases on burn), so minting caps correctly adjust after burns.
- No
_beforeTokenTransferoverride or other transfer locks exist—ERC20Burnable.burn()/burnFrom() remain unrestricted.- Off-chain tokenomics: please confirm your economic model (thresholdSupply, maxSupply, expansion rates) fully accounts for deflationary pressure from burns.
contracts/CATFactory.sol
Outdated
| function getCATAddresses(address _creator, uint256 start, uint256 end) external view returns (address[] memory) { | ||
| address[] memory creatorTokens = administerableTokens[_creator]; | ||
|
|
||
| require(start <= end, "Start index must be less than or equal to end index"); | ||
| require(start < creatorTokens.length, "Start index out of bounds"); | ||
|
|
||
| if (end >= creatorTokens.length) { | ||
| end = creatorTokens.length - 1; | ||
| } | ||
|
|
||
| uint256 resultLength = end - start + 1; | ||
| address[] memory result = new address[](resultLength); | ||
|
|
||
| for (uint256 i = 0; i < resultLength; i++) { | ||
| result[i] = creatorTokens[start + i]; | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Excellent pagination implementation with proper validation.
The rewritten getCATAddresses function correctly implements pagination with robust input validation and boundary handling.
Strengths:
- Proper validation:
start <= endand bounds checking - Safe boundary capping when
endexceeds array length - Correct result array sizing and population
- Gas-efficient for large token arrays
Breaking change consideration:
This changes the function signature, requiring updates to any existing callers.
Verify that all existing callers are updated for the new signature:
🏁 Script executed:
#!/bin/bash
# Description: Find all calls to getCATAddresses to ensure they're updated for the new signature
# Search for function calls to getCATAddresses
rg -A 3 -B 1 "getCATAddresses\(" --type sol --type js --type ts
# Also search in test files
fd -e sol -e js -e ts | xargs rg -l "getCATAddresses" | xargs rg -A 3 -B 1 "getCATAddresses\("Length of output: 1035
Fix: Update ABI entry for getCATAddresses to match new signature
The CATFactory ABI still defines the old single-arg signature, which must be updated to include the start and end parameters.
• web/src/contractsABI/CatFactoryABI.ts
- Replace the line
"getCATAddresses(address)": "2f068701",
with the correct signature for
"getCATAddresses(address,uint256,uint256)". - Regenerate the function selector (e.g., via
web3.eth.abi.encodeFunctionSignature) or re-export the ABI so the hash matches the updated Solidity signature.
🤖 Prompt for AI Agents
In contracts/CATFactory.sol lines 70 to 88, the getCATAddresses function
signature was changed to accept three parameters (address, start, end) for
pagination. To fix the ABI mismatch, update the ABI entry in
web/src/contractsABI/CatFactoryABI.ts by replacing the old single-argument
signature "getCATAddresses(address)" with the new three-argument signature
"getCATAddresses(address,uint256,uint256)". Regenerate the function selector
hash accordingly using a tool like web3.eth.abi.encodeFunctionSignature or
re-export the ABI so the selector matches the updated Solidity function
signature.
💡 Verification agent
🧩 Analysis chain
Address edge case when creator has no tokens.
The pagination logic is mostly correct, but there's an edge case issue. If a creator has no tokens (creatorTokens.length == 0), calling getCATAddresses(_creator, 0, 0) will revert with "Start index out of bounds" even though this seems like a reasonable request.
function getCATAddresses(address _creator, uint256 start, uint256 end) external view returns (address[] memory) {
address[] memory creatorTokens = administerableTokens[_creator];
+ // Return empty array if creator has no tokens
+ if (creatorTokens.length == 0) {
+ return new address[](0);
+ }
+
require(start <= end, "Start index must be less than or equal to end index");
require(start < creatorTokens.length, "Start index out of bounds");Note: This is a breaking change - existing callers will need to update their function calls to include start/end parameters.
🏁 Script executed:
#!/bin/bash
# Search for existing calls to getCATAddresses to assess breaking change impact
echo "Searching for existing getCATAddresses function calls..."
rg -A 3 -B 3 "getCATAddresses" --type sol --type js --type ts
echo "Checking for any tests that might be affected..."
rg -A 5 -B 5 "getCATAddresses" --type js --type tsLength of output: 17958
Add early-exit for zero tokens and update callers for signature change
To avoid reverting when a creator has no tokens, add a guard at the top of getCATAddresses; and because the signature was changed to (address,uint256,uint256), update your ABI and all callers accordingly.
Solidity (contracts/CATFactory.sol):
function getCATAddresses(address _creator, uint256 start, uint256 end) external view returns (address[] memory) {
address[] memory creatorTokens = administerableTokens[_creator];
+ // if the creator has no tokens, return an empty list instead of reverting
+ if (creatorTokens.length == 0) {
+ return new address[](0);
+ }
require(start <= end, "Start index must be less than or equal to end index");
require(start < creatorTokens.length, "Start index out of bounds");
…
}Then update dependent files:
- web/src/contractsABI/CatFactoryABI.ts
• Regenerate or adjust the ABI sogetCATAddressesshows inputs(address,uint256,uint256). - web/src/app/my-cats/page.tsx
• Change your readContract call from:to something like:functionName: "getCATAddresses", args: [userAddress],
(or pick a sensibleargs: [userAddress, 0, ethers.constants.MaxUint256],
endvalue; the contract will clamp it tolength - 1).
Also review any tests or scripts that invoke getCATAddresses(...) to pass the two new pagination parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
contracts/ContributionAccountingToken.sol (1)
13-13: Burn functionality requires additional considerations.The change to
ERC20Burnableinheritance adds publicburn()andburnFrom()functions. This is a significant tokenomics change that could impact the economic model.Based on past review feedback, please ensure:
- Unit tests verify burn functions correctly reduce
totalSupply()maxMintableAmount()properly handles post-burn supply calculations- Documentation clarifies the intended burn behavior and economic impact
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/CATFactory.sol(3 hunks)contracts/ContributionAccountingToken.sol(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/CATFactory.sol
🔇 Additional comments (4)
contracts/ContributionAccountingToken.sol (4)
5-5: Import addition looks good.The import for
ERC20Burnableis correctly added to support the inheritance change.
9-11: Interface definition is well-structured.The
ICATFactoryinterface clearly defines the contract between the CAT and factory for role delegation.
23-23: Factory address storage is appropriately immutable.Using
immutablefor the factory address is correct since it should not change after deployment.
34-34: Constructor parameter and assignment are correctly implemented.The factory address parameter and assignment follow proper patterns for immutable variables.
Also applies to: 44-44
| function _grantMinterRoleFromFactory(address account) external { | ||
| require(msg.sender == factory, "Only factory can call this function"); | ||
| grantRole(MINTER_ROLE, account); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Factory callback function has proper access control but needs additional validation.
The function correctly restricts access to the factory address, but consider additional safeguards.
Consider adding these improvements:
function _grantMinterRoleFromFactory(address account) external {
require(msg.sender == factory, "Only factory can call this function");
+ require(account != address(0), "Cannot grant role to zero address");
+ require(!hasRole(MINTER_ROLE, account), "Account already has minter role");
grantRole(MINTER_ROLE, account);
}This prevents:
- Granting roles to the zero address
- Redundant role grants that could emit misleading events
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function _grantMinterRoleFromFactory(address account) external { | |
| require(msg.sender == factory, "Only factory can call this function"); | |
| grantRole(MINTER_ROLE, account); | |
| } | |
| function _grantMinterRoleFromFactory(address account) external { | |
| require(msg.sender == factory, "Only factory can call this function"); | |
| require(account != address(0), "Cannot grant role to zero address"); | |
| require(!hasRole(MINTER_ROLE, account), "Account already has minter role"); | |
| grantRole(MINTER_ROLE, account); | |
| } |
🤖 Prompt for AI Agents
In contracts/ContributionAccountingToken.sol around lines 106 to 109, the
_grantMinterRoleFromFactory function restricts access to the factory but lacks
validation on the account parameter. Add a require statement to ensure the
account is not the zero address and check if the account already has the
MINTER_ROLE before granting it to avoid redundant role assignments and
misleading events.
| function grantMinterRole(address account) public onlyRole(DEFAULT_ADMIN_ROLE) { | ||
| ICATFactory(factory).grantMinterRole(address(this), account); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify factory delegation doesn't introduce security risks.
The role delegation to the factory changes the trust model. The factory now controls minter role grants, which could be problematic if the factory is compromised or has different access controls.
Please verify that:
- The factory contract has appropriate access controls for
grantMinterRole - The factory cannot be upgraded or replaced in a way that compromises security
- The delegation preserves the intended admin-only access pattern
🏁 Script executed:
#!/bin/bash
# Verify factory contract implementation and access controls
echo "Searching for factory contract implementation..."
rg -A 10 "contract.*Factory" --type sol
echo "Checking factory's grantMinterRole implementation..."
rg -A 10 "function grantMinterRole" --type sol
echo "Looking for factory upgrade mechanisms..."
rg -A 5 "upgrade|proxy|implementation" --type solLength of output: 544
🏁 Script executed:
#!/bin/bash
# Locate all Factory contract files
echo "Locating Factory contract files..."
find . -type f -name '*Factory*.sol'
# Inspect implementation of grantMinterRole across contracts
echo "Checking implementation of grantMinterRole..."
rg -A10 'function\s+grantMinterRole' -g '*.sol'
# Verify access controls within grantMinterRole
echo "Verifying role checks (onlyRole/hasRole) inside grantMinterRole..."
rg -A5 'onlyRole|hasRole' -g '*.sol'
# Search for potential upgrade or proxy mechanisms
echo "Searching for upgrade, proxy, or implementation patterns..."
rg -A5 'upgrade|proxy|implementation|transparent|Beacon' -g '*.sol'Length of output: 6646
Restrict factory’s grantMinterRole and harden the trust model
The current CATFactory.grantMinterRole is an external call with no access control, so anyone can trigger it and grant minter rights on any CAT. Additionally, although we found no proxy or upgrade patterns in the repo, you should ensure that the factory address in the token is immutable (or only updatable by DEFAULT_ADMIN_ROLE) so it can’t be pointed to a malicious implementation.
Please address the following:
- contracts/CATFactory.sol:
- Add an admin check (e.g.
onlyRole(DEFAULT_ADMIN_ROLE)oronlyOwner) tograntMinterRole.
- Add an admin check (e.g.
- contracts/ContributionAccountingToken.sol:
- Make the
factoryreference immutable or guard any setter so only the token’s admin can update it.
- Make the
Suggested diff for CATFactory.sol:
contract CATFactory is AccessControl {
+ bytes32 public constant ADMIN_ROLE = DEFAULT_ADMIN_ROLE;
// ...
- function grantMinterRole(address catAddress, address minter) external {
+ function grantMinterRole(address catAddress, address minter)
+ external
+ onlyRole(ADMIN_ROLE)
{
mintableTokens[minter].push(catAddress);
ContributionAccountingToken(catAddress)._grantMinterRoleFromFactory(minter);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function grantMinterRole(address account) public onlyRole(DEFAULT_ADMIN_ROLE) { | |
| ICATFactory(factory).grantMinterRole(address(this), account); | |
| } | |
| contract CATFactory is AccessControl { | |
| bytes32 public constant ADMIN_ROLE = DEFAULT_ADMIN_ROLE; | |
| // ... | |
| function grantMinterRole(address catAddress, address minter) | |
| external | |
| onlyRole(ADMIN_ROLE) | |
| { | |
| mintableTokens[minter].push(catAddress); | |
| ContributionAccountingToken(catAddress)._grantMinterRoleFromFactory(minter); | |
| } | |
| // ... | |
| } |
🤖 Prompt for AI Agents
In contracts/ContributionAccountingToken.sol around lines 102 to 104, the
grantMinterRole function delegates role granting to the factory contract without
ensuring the factory reference is immutable or access-controlled. To fix this,
make the factory address immutable by declaring it as immutable in the contract
constructor or add a setter function guarded by onlyRole(DEFAULT_ADMIN_ROLE) to
restrict updates. This prevents the factory address from being changed to a
malicious contract, preserving the intended admin-only access pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
contracts/ContributionAccountingToken.sol (1)
113-116: Implement the suggested validation improvements from previous review.The previous review comment correctly identified that this function needs additional validation to prevent granting roles to zero address and avoid redundant role grants.
🧹 Nitpick comments (2)
contracts/ContributionAccountingToken.sol (2)
9-11: Add documentation for the interface.The
ICATFactoryinterface is well-defined but could benefit from documentation to clarify its purpose and the expected behavior of thegrantMinterRolefunction.Consider adding NatSpec documentation:
+/// @title ICATFactory +/// @notice Interface for CAT factory contracts to manage minter roles interface ICATFactory { + /// @notice Grants minter role for a specific CAT token + /// @param catAddress The address of the CAT token contract + /// @param minter The address to grant minter role to function grantMinterRole(address catAddress, address minter) external; }
110-111: Add input validation before delegating to factory.While the delegation to the immutable factory is architecturally sound, consider adding input validation to prevent unnecessary external calls.
Add validation before the factory call:
function grantMinterRole(address account) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(account != address(0), "Cannot grant role to zero address"); + require(!hasRole(MINTER_ROLE, account), "Account already has minter role"); ICATFactory(factory).grantMinterRole(address(this), account); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
contracts/ContributionAccountingToken.sol(6 hunks)
🔇 Additional comments (2)
contracts/ContributionAccountingToken.sol (2)
34-34: Excellent security improvement with immutable factory.Making the
factoryfield immutable addresses the security concern raised in previous reviews. This prevents the factory address from being changed after deployment, maintaining the integrity of the role delegation mechanism.Also applies to: 44-44
72-72: Mint function logic is correct and well-validated.The addition of the
maxMintableAmount()validation ensures economic constraints are respected. The fee calculation properly deducts fees from the user's portion while the total minted amount (user + fees) equals the validated amount.Also applies to: 75-75, 77-78
contracts/CATFactory.sol
Outdated
| require(msg.sender == catAddress, "Only CAT contract can call this function"); | ||
|
|
||
| // Verify that the admin has the admin role in the CAT contract | ||
| require(ContributionAccountingToken(catAddress).hasRole(ContributionAccountingToken(catAddress).DEFAULT_ADMIN_ROLE(), admin), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment in discord.
| function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { | ||
|
|
||
| // Minting fee calculation | ||
| function userAmountAfterFees(uint256 amount) public pure returns (uint256) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is being used only once and it is a very small function. Therefore, it is better to remove this function and just move lines 67 and 68 directly there where they are needed (around line 74).
| ICATFactory(factory).grantMinterRole(address(this), msg.sender, account); | ||
| } | ||
|
|
||
| function grantMinterRoleFromFactory(address account) external { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment in Discord. This function is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🔭 Outside diff range comments (2)
contracts/CATFactory.sol (2)
17-17:⚠️ Potential issueInvalid constructor call –
Ownablehas no parameters
Ownable(OpenZeppelin ≥v4.x) exposes a zero-arg constructor.
Passingmsg.senderwill cause a compilation error:constructor() Ownable(msg.sender) {} ^^^^^^^^^^^^^- constructor() Ownable(msg.sender) {} + constructor() Ownable() {}
35-43: 🛠️ Refactor suggestionPrefer explicit token-registry before externalising factory address
Passing
address(this)toContributionAccountingTokenhard-couples the token to the current factory instance – good.
However, the factory does not keep a canonical registry of created CAT addresses. Subsequent functions (seeonMinterRoleGranted) rely on trustingcatAddresswithout verifying it belongs to the factory.
Consider adding:mapping(address => bool) private isFactoryToken;and flagging the new token immediately after deployment:
isFactoryToken[catAddress] = true;This enables cheap
require(isFactoryToken[msg.sender], "Unrecognised CAT");checks elsewhere.
🧹 Nitpick comments (2)
contracts/CATFactory.sol (2)
81-98: Pagination logic OK but gas can be halvedCurrent copying loop is
O(n).
You can micro-optimise with unchecked increments and local caching:- for (uint256 i = 0; i < resultLength; i++) { - result[i] = creatorTokens[start + i]; - } + for (uint256 i; i < resultLength;) { + result[i] = creatorTokens[start + i]; + unchecked { ++i; } + }Minor, but can matter for large arrays.
100-118: Duplicate copy–paste – extract internal helper
getMinterCATAddressesrepeatsgetCreatorCATAddresses.
Factor the common pagination into a private pure/internal function to reduce byte-code and maintenance burden.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/CATFactory.sol(4 hunks)contracts/ContributionAccountingToken.sol(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/ContributionAccountingToken.sol
🧰 Additional context used
🧠 Learnings (1)
contracts/CATFactory.sol (1)
Learnt from: ceilican
PR: StabilityNexus/Clowder#66
File: contracts/CATFactory.sol:70-74
Timestamp: 2025-06-09T18:19:25.601Z
Learning: When implementing array slicing/pagination functions in Solidity, use `require(start <= array.length, "Start index out of bounds");` instead of `require(start < array.length, "Start index out of bounds");` to allow valid empty ranges at array boundaries and handle empty arrays gracefully without special case logic.
🔇 Additional comments (2)
contracts/CATFactory.sol (2)
47-49: Token-id emitted before increment – off-by-one?
_nextTokenIdis emitted then incremented.
If you intend the first CAT to have id1, increment before emitting; if0is acceptable, leave as-is but document it.
[nitpick]
120-125: Considerview→pure
getCreatorCATCount/getMinterCATCountread from storage –viewis correct.
No change required.
contracts/CATFactory.sol
Outdated
| mintableTokens[minter].push(catAddress); // Update mintable tokens mapping | ||
| function onMinterRoleGranted(address catAddress, address minter) external { | ||
| // Verify that the caller is the CAT contract | ||
| require(msg.sender == catAddress, "Only CAT contract can call this function"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this require. And we don't need the catAddress argument in line 59. we can just use msg.sender in lines 66 and 70.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
contracts/CATFactory.sol
Outdated
|
|
||
| // Check if catAddress already exists in the array | ||
| address[] storage minterTokens = mintableTokens[minter]; | ||
| for (uint256 i = 0; i < minterTokens.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop can become too expensive when minterTokens becomes too long. Instead, use a mapping(address => mapping(address => bool) to store whether a given address has already been registered as a minter for a given CAT. Then the check can be done in constant time instead of linear time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
contracts/CATFactory.sol
Outdated
| function getMinterCATAddresses(address _minter, uint256 start, uint256 end) external view returns (address[] memory) { | ||
| address[] memory minterTokens = mintableTokens[_minter]; | ||
|
|
||
| require(start <= end, "Start index must be less than or equal to end index"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is code duplication between getMinter... and getCreator.... the only difference between them is that one uses mintableTokens and the other uses administerableTokens.
Eliminate the code duplication by factoring out an internal auxiliary getSubArray function that takes an array as an argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
contracts/CATFactory.sol (3)
79-88: Ambiguous inclusive/exclusive contract – document itThe function treats
endas exclusive (resultLength = end - start), yet the NatSpec comment simply says “ending index”.
Explicitly state “endis exclusive” (or switch to inclusive semantics) to prevent caller off-by-ones.
80-83: Symmetric bounds check forendYou require
start <= tokens.lengthbut allow anyend; only afterwards you clamp it.
Gas is cheaper and revert reasons clearer if you validate first:require(end <= tokens.length, "End index out of bounds");and drop the manual clamping block.
This keeps the API predictable (no silent mutation ofend).
11-14: Public dynamic-array state vars leak unbounded data
administerableTokensandmintableTokensarepublic, which auto-generates getters that return one element at a time; external consumers must make N calls to read the whole list, leading to fragile off-chain patterns and high RPC traffic.Given you already expose paginated getters, consider marking these mappings
internal/private.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/CATFactory.sol(3 hunks)contracts/ContributionAccountingToken.sol(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/ContributionAccountingToken.sol
🧰 Additional context used
🧠 Learnings (1)
contracts/CATFactory.sol (1)
Learnt from: ceilican
PR: StabilityNexus/Clowder#66
File: contracts/CATFactory.sol:70-74
Timestamp: 2025-06-09T18:19:25.601Z
Learning: When implementing array slicing/pagination functions in Solidity, use `require(start <= array.length, "Start index out of bounds");` instead of `require(start < array.length, "Start index out of bounds");` to allow valid empty ranges at array boundaries and handle empty arrays gracefully without special case logic.
contracts/CATFactory.sol
Outdated
| function onMinterRoleGranted(address minter) external { | ||
| if (isMinterForCAT[minter][msg.sender]) { | ||
| return; | ||
| } | ||
| isMinterForCAT[minter][msg.sender] = true; | ||
| mintableTokens[minter].push(msg.sender); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing gate → any contract can spoof onMinterRoleGranted calls
onMinterRoleGranted relies solely on msg.sender being the CAT that just granted a role, but there is no verification that msg.sender is actually a CAT created by this factory.
A malicious contract can therefore execute
factory.onMinterRoleGranted(victimMinter);and pollute mintableTokens[victimMinter], confusing indexers / UIs and breaking assumptions made off-chain.
Diff sketch (one way to fix):
+ // Track all CATs created by this factory
+ mapping(address => bool) private _isFactoryCAT;
function createCAT(...) public returns (address) {
ContributionAccountingToken newCAT = new ContributionAccountingToken(
msg.sender,
address(this),
...
);
address catAddress = address(newCAT);
+ _isFactoryCAT[catAddress] = true; // ✅ register once
...
}
function onMinterRoleGranted(address minter) external {
- if (isMinterForCAT[minter][msg.sender]) {
- return;
- }
+ require(_isFactoryCAT[msg.sender], "Caller is not a factory CAT");
+ if (isMinterForCAT[minter][msg.sender]) return;
...
}🤖 Prompt for AI Agents
In contracts/CATFactory.sol around lines 59 to 65, the onMinterRoleGranted
function lacks verification that msg.sender is a CAT created by this factory,
allowing any contract to spoof calls and corrupt mintableTokens data. To fix
this, add a check that msg.sender is in the factory's list or mapping of created
CAT contracts before proceeding. Reject calls from addresses not recognized as
valid CATs to prevent unauthorized updates.
Zahnentferner
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look for superfluous obvious comments and remove them.
Look for comments that could be written to the right side of some code line instead of above that code line, and move them there.
contracts/CATFactory.sol
Outdated
| return; | ||
| } | ||
| function onMinterRoleGranted(address minter) external { | ||
| if (isMinterForCAT[minter][msg.sender]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!isMinterForCAT[minter][msg.sender]) {
isMinterForCAT[minter][msg.sender] = true;
mintableTokens[minter].push(msg.sender);
}
Generally, try to avoid "return" when possible, because they tend to make the code more difficult to understand.
contracts/CATFactory.sol
Outdated
| uint256 private _nextTokenId; | ||
|
|
||
| // Mapping from owner address to token addresses | ||
| mapping(address => address[]) public administerableTokens; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make the variable names a bit more coherent, could you rename administerableTokens to creatorTokens and mintableTokens to minterTokens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
contracts/CATFactory.sol (1)
57-61:⚠️ Potential issue
onMinterRoleGrantedis wide-open to spoofing – any contract can polluteminterTokens.Nothing restricts who can call this function. A malicious contract can simply execute
factory.onMinterRoleGranted(victimAddress)and insert itself intominterTokens[victimAddress], corrupting off-chain accounting and UI logic.Tighten the gate by tracking CATs created by this factory and rejecting calls from unknown senders. A minimal fix:
+ // Track all CATs the factory has created + mapping(address => bool) private _isFactoryCAT; + function createCAT( … ) public returns (address) { ContributionAccountingToken newCAT = new ContributionAccountingToken( msg.sender, address(this), … ); address catAddress = address(newCAT); + _isFactoryCAT[catAddress] = true; // ✅ register … } function onMinterRoleGranted(address minter) external { + require(_isFactoryCAT[msg.sender], "Caller is not a factory CAT"); + require(minter != address(0), "Zero minter"); if (!isMinterForCAT[minter][msg.sender]) { isMinterForCAT[minter][msg.sender] = true; minterTokens[minter].push(msg.sender); } }Without this guard the bookkeeping in
minterTokensandisMinterForCATcannot be trusted.
🧹 Nitpick comments (2)
contracts/CATFactory.sol (2)
69-83: Micro-optimisation & clarity for_getSubArray.
- The loop index math is safe but can save a few gas by using
uncheckedinside theforsince bounds are enforced above.- Consider documenting that
endis exclusive; callers familiar with Solidity’s usual inclusive ranges may mis-interpret.Optional tweak:
- for (uint256 i = 0; i < resultLength; i++) { - result[i] = tokens[start + i]; - } + for (uint256 i; i < resultLength; ) { + result[i] = tokens[start + i]; + unchecked { ++i; } + }Not critical, but worth it if these helpers are called frequently on-chain.
10-14: Public array-valued mappings expose only single-element getters.Declaring
creatorTokensandminterTokensaspublicgives autogenerated getters of the formcreatorTokens(address,uint) → address, not full-array views. Because you already expose dedicated pagination/count functions, thepublicvisibility adds no utility and slightly bloats the ABI.- mapping(address => address[]) public creatorTokens; - mapping(address => address[]) public minterTokens; + mapping(address => address[]) private creatorTokens; + mapping(address => address[]) private minterTokens;Keeps the external surface minimal while all required data remains accessible via your new helpers.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/CATFactory.sol(3 hunks)contracts/ContributionAccountingToken.sol(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/ContributionAccountingToken.sol
🧰 Additional context used
🧠 Learnings (1)
contracts/CATFactory.sol (1)
Learnt from: ceilican
PR: StabilityNexus/Clowder#66
File: contracts/CATFactory.sol:70-74
Timestamp: 2025-06-09T18:19:25.601Z
Learning: When implementing array slicing/pagination functions in Solidity, use `require(start <= array.length, "Start index out of bounds");` instead of `require(start < array.length, "Start index out of bounds");` to allow valid empty ranges at array boundaries and handle empty arrays gracefully without special case logic.
Summary by CodeRabbit