-
Notifications
You must be signed in to change notification settings - Fork 88
Implement AccessController and ModuleRegistry for IPAccount Execution Model #4
Conversation
library AccessPermission { | ||
// ABSTAIN means having not enough information to make decision at current level, | ||
// deferred decision to up level permission. | ||
uint8 public constant ABSTAIN = 0; |
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.
Wondering what case would require ABSTAIN?
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.
Oh I see in AccessController.sol
// If specific function permission is ABSTAIN, check module level permission
if (permissions[ipAccount_][signer_][to_][func_] == AccessPermission.ABSTAIN) {
// Return true if allow to call all functions of the module
if (permissions[ipAccount_][signer_][to_][bytes4(0)] == AccessPermission.ALLOW) {
return true;
}
// If module level permission is ABSTAIN, check transaction signer level permission
if (permissions[ipAccount_][signer_][to_][bytes4(0)] == AccessPermission.ABSTAIN) {
// Return true if the ipAccount allow the signer can call all functions of all modules
// Otherwise, return false
return permissions[ipAccount_][signer_][address(0)][bytes4(0)] == AccessPermission.ALLOW;
}
return false;
}
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.
Would be nice to standardize errors for mocks as well
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.
Good point, might handle in seperated PR.
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.
LGTM, nice testing
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.
LGTM
address public IP_ACCOUNT_REGISTRY; | ||
address public MODULE_REGISTRY; | ||
|
||
mapping(address => mapping(address => mapping(address => mapping(bytes4 => uint8)))) public permissions; |
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.
Probably helpful to document and use named mapping parameters
https://ethereum.stackexchange.com/questions/51629/how-to-name-the-arguments-in-mapping
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.
Good point! will change in the next PR.
/// | ||
/// Each policy is represented as a mapping from an IP account address to a signer address to a recipient | ||
/// address to a function selector to a permission level. | ||
/// The permission level can be 0 (ABSTAIN), 1 (ALLOW), or 2 (DENY). |
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.
Can we make an enum for this used through a library?
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.
Currently these values are defined in a library, but as constant. I was thinking about using enum instead. but feel constants would more clear than enum, since it reflect actual value in storage, for example 0 for abstain is default value.
/// @param to_ The recipient of the transaction (support wildcard permission) | ||
/// @param func_ The function selector (support wildcard permission) | ||
/// @param permission_ The permission level (0 => ABSTAIN, 1 => ALLOW, 3 => DENY) | ||
function setPermission(address ipAccount_, address signer_, address to_, bytes4 func_, uint8 permission_) 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.
We should use caller_ instead of signer_, signer is pretty confusing
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.
yeah, caller is good.
/// @param func_ The function selector. | ||
/// @return The permission level for the specific function call. | ||
function getPermission( | ||
address ipAccount_, |
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.
I think we agreed to stop using underscore suffixes
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 code was actual writen before, will have another PR handle all code style.
@@ -34,7 +34,6 @@ contract IPAccountImpl is IERC165, IIPAccount { | |||
// TODO: can only be called by IPAccountRegistry | |||
function initialize(address accessController_) external { | |||
require(accessController_ != address(0), "Invalid access controller"); |
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.
Can we go back to using custom errors? Not sure why we're reverting. I think we agreed on that from alpha
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.
yes, will use custom error instead in code style pr.
if (ipAccountAddress_ == address(0)) return false; | ||
if (ipAccountAddress_.code.length == 0) return false; | ||
if (!ERC165Checker.supportsERC165(ipAccountAddress_)) return false; | ||
if (!ERC165Checker.supportsInterface(ipAccountAddress_, type(IERC6551Account).interfaceId)) return false; |
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.
Isn't this redundant
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.
yeah, this check should be able to be removed it.
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.
I think this contract should inherit from OZ's AccessManager (later on), which shares some the core logic. That way we have less code to audit
https://docs.openzeppelin.com/contracts/5.x/api/access#AccessManager
/// | ||
/// Each policy is represented as a mapping from an IP account address to a signer address to a recipient | ||
/// address to a function selector to a permission level. | ||
/// The permission level can be 0 (ABSTAIN), 1 (ALLOW), or 2 (DENY). |
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.
ABSTAIN is confusing as a term
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.
In access control systems, the term 'ABSTAIN' is commonly used to represent a specific decision state. Unlike 'ALLOW' or 'DENY', which are clear actions, 'ABSTAIN' indicates that the system does not make a definitive decision. This could be due to a lack of sufficient information, rules, or policies applicable to a particular request. It's a neutral state where the system neither grants nor denies access but leaves the decision to be made by a higher level in the decision-making hierarchy.
address public IP_ACCOUNT_REGISTRY; | ||
address public MODULE_REGISTRY; | ||
|
||
mapping(address => mapping(address => mapping(address => mapping(bytes4 => uint8)))) public permissions; |
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 5 times nested mapping feels like a lot. Opportunities for optimization
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.
Yes, we might consider if there is a room for optimization later.
Based on my in-depth analysis of OpenZeppelin's AccessManager, I've identified certain limitations that make it less suitable for our project's specific requirements. Primarily, the AccessManager implements Role-Based Access Control (RBAC), which lacks support for wildcards. This limitation necessitates setting permissions on a per-function basis, which can be cumbersome and less efficient for our use case. Additionally, the AccessManager framework requires that all functions include the 'AccessManaged.restricted' modifier. Implementing this across community-developed modules would impose a significant overhead, both in terms of development effort and runtime costs. Given these considerations, although AccessManager offers robust access control, its structure and operational requirements do not align optimally with our project's needs and constraints. |
This PR introduces the implementation of AccessController and ModuleRegistry to enhance the execution model of IPAccount. The new model supports multiple call paths with robust access control mechanisms.
The execution models supported include:
These changes ensure that the system is flexible and secure, allowing for complex interactions while maintaining control over access permissions.
In addition, this PR includes comprehensive test coverage to ensure the reliability and stability of the new features. The tests cover all new functionality, providing confidence in the robustness of the implementation.