Skip to content

Conversation

@harshkumarrai
Copy link

@harshkumarrai harshkumarrai commented Jan 10, 2026

Fixes #6143

This PR adds an optional AccessControlBatch extension that allows
granting and revoking multiple roles to multiple accounts in a single
transaction, without modifying the core AccessControl contract.

The change addresses the batch role assignment use case discussed in #6143.

  • Tests
  • Documentation
  • Changeset entry

@harshkumarrai harshkumarrai requested a review from a team as a code owner January 10, 2026 12:01
@changeset-bot
Copy link

changeset-bot bot commented Jan 10, 2026

🦋 Changeset detected

Latest commit: 8b34161

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 10, 2026

Walkthrough

Adds an abstract AccessControlBatch extension providing grantRoles(bytes32[] calldata, address[] calldata) and revokeRoles(bytes32[] calldata, address[] calldata) for batch role assignment and removal, both guarded by onlyRole(DEFAULT_ADMIN_ROLE). Each function requires equal array lengths (reverting with "AccessControlBatch: length mismatch"), rejects the zero address (reverting with "AccessControlBatch: invalid account"), and calls _grantRole/_revokeRole per pair. Also adds AccessControlBatchMock and tests covering success and described failure modes.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding an AccessControlBatch extension for batch role assignment.
Linked Issues check ✅ Passed The PR implements batch role grant/revoke functionality via an optional extension, addressing the core requirement from issue #6143 to grant multiple roles to multiple users in a single transaction.
Out of Scope Changes check ✅ Passed All changes are in scope: new AccessControlBatch extension, mock contract for testing, comprehensive test suite, and changeset entry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description clearly relates to the changeset, describing the addition of an AccessControlBatch extension for batch role assignment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @contracts/access/extensions/AccessControlBatch.sol:
- Around line 19-27: The grantRoles function allows granting roles to addresses
without checking for the zero address; add a non-zero validation inside the loop
(or a separate pre-check) to ensure accounts[i] != address(0) before calling
_grantRole and revert with a clear message like "AccessControlBatch: zero
address" when violated; update the grantRoles function (referencing grantRoles,
roles, accounts, and _grantRole) to perform this check for each account to
prevent accidental grants to address(0).
- Around line 36-44: The revokeRoles function lacks a zero-address check for
accounts, causing inconsistency with grantRoles; before calling _revokeRole in
revokeRoles(bytes32[] calldata roles, address[] calldata accounts) add a
validation that accounts[i] != address(0) (revert with a clear message like
"AccessControlBatch: zero address") for each entry (do the check inside the for
loop prior to _revokeRole) so accidental operations on the zero address are
prevented and behavior matches grantRoles.
- Around line 19-44: Add validation in grantRoles and revokeRoles to reject the
zero address: inside the for-loop that iterates over roles/accounts in
grantRoles(bytes32[] calldata roles, address[] calldata accounts) and
revokeRoles(...) check if accounts[i] == address(0) and revert with a clear
message (e.g., "AccessControlBatch: invalid account") before calling
_grantRole(roles[i], accounts[i]) or _revokeRole(roles[i], accounts[i]); ensure
both functions enforce the same check and keep the existing length mismatch
check and onlyRole modifier.
🧹 Nitpick comments (3)
test/access/AccessControlBatch.test.js (1)

40-42: Consider checking the specific revert reason.

The test validates that non-admin access is blocked, but using a more specific assertion would improve test clarity. Consider verifying the exact AccessControl error message.

📝 More specific assertion
-  it('reverts when caller is not admin', async function () {
-    await expect(contract.connect(user1).grantRoles([ROLE_A], [user1.address])).to.be.reverted;
-  });
+  it('reverts when caller is not admin', async function () {
+    await expect(contract.connect(user1).grantRoles([ROLE_A], [user1.address]))
+      .to.be.revertedWithCustomError(contract, 'AccessControlUnauthorizedAccount')
+      .withArgs(user1.address, await contract.DEFAULT_ADMIN_ROLE());
+  });
contracts/access/extensions/AccessControlBatch.sol (2)

20-22: Consider using custom errors for gas efficiency.

String reverts consume more gas than custom errors. OpenZeppelin contracts typically use custom errors for better gas optimization.

⚡ Custom error implementation

Add this error declaration near the top of the contract:

error AccessControlBatchLengthMismatch(uint256 rolesLength, uint256 accountsLength);

Then update the revert:

-    if (roles.length != accounts.length) {
-        revert("AccessControlBatch: length mismatch");
-    }
+    if (roles.length != accounts.length) {
+        revert AccessControlBatchLengthMismatch(roles.length, accounts.length);
+    }

37-39: Consider using custom errors for gas efficiency.

Same recommendation as grantRoles: using custom errors would improve gas efficiency. The same custom error can be reused for both functions.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1a0a67 and 3f5485a.

📒 Files selected for processing (3)
  • contracts/access/extensions/AccessControlBatch.sol
  • contracts/mocks/AccessControlBatchMock.sol
  • test/access/AccessControlBatch.test.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-29T13:16:08.640Z
Learnt from: Amxx
Repo: OpenZeppelin/openzeppelin-contracts PR: 5904
File: contracts/mocks/crosschain/ERC7786RecipientMock.sol:12-14
Timestamp: 2025-08-29T13:16:08.640Z
Learning: In OpenZeppelin contracts, mock contracts (like ERC7786RecipientMock) don't require input validation such as zero-address checks in constructors, as they are only used for testing purposes in controlled environments.

Applied to files:

  • contracts/mocks/AccessControlBatchMock.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Redirect rules - solidity-contracts
  • GitHub Check: Header rules - solidity-contracts
  • GitHub Check: Pages changed - solidity-contracts
  • GitHub Check: slither
  • GitHub Check: tests-foundry
  • GitHub Check: tests
  • GitHub Check: coverage
  • GitHub Check: tests-upgradeable
  • GitHub Check: halmos
🔇 Additional comments (5)
contracts/mocks/AccessControlBatchMock.sol (1)

1-10: LGTM! Clean mock implementation.

The mock correctly extends AccessControlBatch and initializes the admin role. The absence of zero-address validation is acceptable for mock contracts used in testing.

test/access/AccessControlBatch.test.js (3)

4-16: LGTM! Test setup is well-structured.

The setup correctly initializes test roles using ethers.id and deploys the mock contract with proper admin configuration.


18-23: LGTM! Batch grant test is comprehensive.

The test validates that multiple roles can be granted in a single transaction and correctly asserts both role assignments.


25-32: LGTM! Batch revoke test is thorough.

The test properly validates the batch revocation flow by first granting roles and then verifying their removal.

contracts/access/extensions/AccessControlBatch.sol (1)

1-11: LGTM! Well-documented extension contract.

The contract header properly documents that this is an optional extension and correctly extends AccessControl.

@harshkumarrai
Copy link
Author

@coderabbitai please re-review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 10, 2026

@harshkumarrai: I'll re-review the PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (4)
test/access/AccessControlBatch.test.js (4)

18-23: Consider using more idiomatic chai assertions.

The test correctly verifies batch role grants. For boolean assertions, consider using .to.be.true instead of .to.equal(true) for more idiomatic chai syntax.

✨ Optional style improvement
-    expect(await contract.hasRole(ROLE_A, user1.address)).to.equal(true);
-    expect(await contract.hasRole(ROLE_B, user2.address)).to.equal(true);
+    expect(await contract.hasRole(ROLE_A, user1.address)).to.be.true;
+    expect(await contract.hasRole(ROLE_B, user2.address)).to.be.true;

25-32: Consider using more idiomatic chai assertions.

The test correctly verifies batch role revocation. For consistency with chai best practices, consider using .to.be.false instead of .to.equal(false).

✨ Optional style improvement
-    expect(await contract.hasRole(ROLE_A, user1.address)).to.equal(false);
-    expect(await contract.hasRole(ROLE_B, user2.address)).to.equal(false);
+    expect(await contract.hasRole(ROLE_A, user1.address)).to.be.false;
+    expect(await contract.hasRole(ROLE_B, user2.address)).to.be.false;

57-59: Consider checking specific revert reason for more precise test validation.

The test correctly verifies that non-admin callers cannot grant roles, but using a generic .to.be.reverted is less precise. Consider verifying the specific AccessControl error message to ensure the correct validation is being triggered.

♻️ More precise assertion
   it('reverts when caller is not admin', async function () {
-    await expect(contract.connect(user1).grantRoles([ROLE_A], [user1.address])).to.be.reverted;
+    await expect(contract.connect(user1).grantRoles([ROLE_A], [user1.address]))
+      .to.be.revertedWithCustomError(contract, 'AccessControlUnauthorizedAccount')
+      .withArgs(user1.address, await contract.DEFAULT_ADMIN_ROLE());
   });

Note: Adjust the error name/format based on the actual AccessControl implementation in use.


4-65: Consider adding event emission tests and additional edge case coverage.

The current test suite covers the main success and failure paths well. To improve robustness, consider adding:

  1. Recommended: Event emission verification - AccessControl typically emits RoleGranted and RoleRevoked events. Verify these are emitted correctly for each role in the batch.

  2. Optional: Additional edge cases:

    • Empty arrays (both roles and accounts)
    • Granting the same role to multiple accounts
    • Granting multiple roles to the same account
    • Re-granting an already granted role
    • Revoking a role that was never granted
📝 Example event verification test
it('emits RoleGranted events for each role', async function () {
  await expect(contract.connect(admin).grantRoles([ROLE_A, ROLE_B], [user1.address, user2.address]))
    .to.emit(contract, 'RoleGranted')
    .withArgs(ROLE_A, user1.address, admin.address)
    .to.emit(contract, 'RoleGranted')
    .withArgs(ROLE_B, user2.address, admin.address);
});

it('emits RoleRevoked events for each role', async function () {
  await contract.connect(admin).grantRoles([ROLE_A, ROLE_B], [user1.address, user2.address]);
  
  await expect(contract.connect(admin).revokeRoles([ROLE_A, ROLE_B], [user1.address, user2.address]))
    .to.emit(contract, 'RoleRevoked')
    .withArgs(ROLE_A, user1.address, admin.address)
    .to.emit(contract, 'RoleRevoked')
    .withArgs(ROLE_B, user2.address, admin.address);
});
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f5485a and b32593c.

📒 Files selected for processing (3)
  • .changeset/upset-teeth-play.md
  • contracts/access/extensions/AccessControlBatch.sol
  • test/access/AccessControlBatch.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/access/extensions/AccessControlBatch.sol
🔇 Additional comments (2)
.changeset/upset-teeth-play.md (1)

1-5: LGTM!

The changeset entry correctly specifies a minor version bump for adding a new optional extension, and the description accurately reflects the changes.

test/access/AccessControlBatch.test.js (1)

1-16: LGTM!

The test setup is well-structured with appropriate imports, role definitions using ethers.id(), and clean contract deployment in the beforeEach hook.

@harshkumarrai
Copy link
Author

@coderabbitai please re-review the changes

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 10, 2026

@harshkumarrai: I'll re-review the changes in the PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AccessControlv2 using ERC827 token standard

1 participant