Glamorous Canvas Camel
Medium
The ReputationMarket
contract, which inherits from AccessControl
, uses the updateOwner
function to transfer ownership. This function relies on getRoleMember(OWNER_ROLE, 0)
to identify the current owner for role revocation. However, the AccessControlEnumerableUpgradeable
contract from OpenZeppelin specifies that role members are not stored in any particular order, and their ordering may change. This means that getRoleMember(OWNER_ROLE, 0)
may not reliably return the current owner (msg.sender
), leading to improper revocation of the owner role from unintended addresses. This can result in ownership mismanagement, unauthorized access, or loss of control over sensitive functions in the ReputationMarket
contract.
the updateOwner
function is implemented as follows:
function updateOwner(address owner) external onlyOwner {
_revokeRole(OWNER_ROLE, getRoleMember(OWNER_ROLE, 0));
_grantRole(OWNER_ROLE, owner);
}
This function assumes that:
- There is only one address with the
OWNER_ROLE
. - The owner is always at index
0
in the list of role members.
However, the AccessControlEnumerableUpgradeable
contract (from OpenZeppelin) used by AccessControl
manages role members using an EnumerableSet
, which does not guarantee any specific ordering of elements. The relevant part of the OpenZeppelin code is:
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].at(index);
}
The key point is that "Role bearers are not sorted in any particular way, and their ordering may change at any point."
Therefore, using getRoleMember(OWNER_ROLE, 0)
does not reliably return the current owner (msg.sender
). If multiple addresses have the OWNER_ROLE
, the address at index 0
may not be the caller. This can lead to revoking the owner role from an unintended address.
No response
No response
-
Multiple Owners: Assume multiple addresses hold the
OWNER_ROLE
due to prior role assignments. -
Ownership Transfer Attempt: An owner (
Owner1
) callsupdateOwner(newOwnerAddress)
to transfer ownership. -
Incorrect Role Revocation: The function revokes the
OWNER_ROLE
from the address at index0
, which might not beOwner1
, but another owner (Owner2
). -
Ownership Mismanagement:
Owner1
still retains theOWNER_ROLE
.Owner2
loses theOWNER_ROLE
unexpectedly.- The new owner (
newOwnerAddress
) gains theOWNER_ROLE
.
-
Potential Unauthorized Access: Unintended addresses may retain owner privileges, leading to unauthorized access to sensitive functions in
ReputationMarket
, such as upgrading the contract via_authorizeUpgrade
:function _authorizeUpgrade( address newImplementation ) internal override onlyOwner onlyNonZeroAddress(newImplementation) { // Intentionally left blank to ensure onlyOwner and zeroCheck modifiers run }
- Ownership Mismanagement: Ownership transfer may not work as intended, leading to multiple owners or loss of control by the intended owner.
- Unauthorized Access: Addresses that should no longer have the
OWNER_ROLE
may retain it, allowing them to perform sensitive operations.
-
Setup: Two addresses,
Owner1
andOwner2
, hold theOWNER_ROLE
:// Both Owner1 and Owner2 have been granted OWNER_ROLE
-
Ownership Transfer Attempt:
Owner1
tries to transfer ownership toNewOwner
:reputationMarket.updateOwner(newOwnerAddress);
-
Function Execution in
AccessControl
:function updateOwner(address owner) external onlyOwner { // Revokes OWNER_ROLE from the address at index 0 (could be Owner2) _revokeRole(OWNER_ROLE, getRoleMember(OWNER_ROLE, 0)); _grantRole(OWNER_ROLE, owner); // Grants OWNER_ROLE to NewOwner }
-
Outcome:
Owner2
(at index0
) loses theOWNER_ROLE
.Owner1
retains theOWNER_ROLE
.NewOwner
gains theOWNER_ROLE
.- This results in
Owner1
andNewOwner
both having theOWNER_ROLE
, which may not be intended.
-
Resulting Issues:
Owner1
did not lose their owner privileges.Owner2
lost ownership unintentionally.- Potential for unauthorized actions by unintended owners.
Modify the updateOwner
function to revoke the OWNER_ROLE
from the caller (msg.sender
) instead of using getRoleMember
:
function updateOwner(address owner) external onlyOwner {
_revokeRole(OWNER_ROLE, msg.sender);
_grantRole(OWNER_ROLE, owner);
}
This ensures that the owner transferring ownership loses their privileges, and the new owner gains them.