Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Implement AccessController and ModuleRegistry for IPAccount Execution Model #4

Merged
merged 2 commits into from
Jan 20, 2024

Conversation

kingster-will
Copy link
Contributor

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:

  1. Direct interaction from IPAccount to Module.
  2. Interaction from Module to IPAccount and then to another Module.
  3. Interaction from Orchestrator Module to IPAccount and then interact with multiple modules.

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.

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;
Copy link
Contributor

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?

Copy link
Contributor

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;
}

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

@jdubpark jdubpark left a comment

Choose a reason for hiding this comment

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

LGTM, nice testing

Copy link
Contributor

@leeren leeren left a 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;
Copy link
Contributor

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

Copy link
Contributor Author

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).
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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

Copy link
Contributor Author

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_,
Copy link
Contributor

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

Copy link
Contributor Author

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");
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this redundant

Copy link
Contributor Author

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.

Copy link
Contributor

@Ramarti Ramarti left a 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).
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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.

@kingster-will kingster-will merged commit 25827dd into storyprotocol:main Jan 20, 2024
1 check passed
@kingster-will kingster-will deleted the access branch January 20, 2024 03:13
@kingster-will
Copy link
Contributor Author

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

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.

@kingster-will kingster-will self-assigned this Jan 20, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants