Skip to content
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

feat: Extended PM with permission owner and delegation capabilities #604

Open
wants to merge 95 commits into
base: develop
Choose a base branch
from

Conversation

nivida
Copy link
Contributor

@nivida nivida commented Aug 28, 2024

Description

Please include a summary of the change and be sure you follow the contributions rules we do provide here

Task ID: OS-?

Type of change

See the framework lifecycle in packages/contracts/docs/framework-lifecycle to decide what kind of change this pull request is.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • I have selected the correct base branch.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings.
  • Any dependent changes have been merged and published in downstream modules.
  • I ran all tests with success and extended them if necessary.
  • I have updated the CHANGELOG.md file in the root folder.
  • I have updated the DEPLOYMENT_CHECKLIST file in the root folder.
  • I have updated the UPDATE_CHECKLIST file in the root folder.
  • I have updated the Subgraph and added a QA URL to the description of this PR.
  • I have created a follow-up task to update the Developer Portal with the changes made in this PR.

Copy link
Contributor

@novaknole novaknole left a comment

Choose a reason for hiding this comment

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

1st Round ^_^

novaknole and others added 18 commits September 17, 2024 10:03
novaknole and others added 9 commits September 17, 2024 15:33
* fix more tests and cleaner

* test fixes + permissionFrozen for multi target + more fixes

* improve natspec
* fix: remove .only

* fix: 63/64 rule tests

* feat: add tests for undelegate function

* fix typechain coverage

* fix: tets

* feat: add couple of tests

---------

Co-authored-by: Giorgi Lagidze <[email protected]>
* fix pm contract wiwth tests

* Update packages/contracts/src/core/permission/PermissionManager.sol

Co-authored-by: Claudia Barcelo <[email protected]>

* Update packages/contracts/src/core/permission/PermissionManager.sol

Co-authored-by: Claudia Barcelo <[email protected]>

* Update packages/contracts/test/core/permission/permission-manager.ts

Co-authored-by: Claudia Barcelo <[email protected]>

* add extra tests for delegation

* fix checkOwner for delegation

* separate tests

* more gas efficient approach

* efficient gas approach + fix removeOwner with tests

* add more tests

---------

Co-authored-by: Claudia Barcelo <[email protected]>
* executor interface

* add tests for iexecutor

* test

* action import free level

* Update packages/contracts/test/framework/dao/dao-factory.ts

Co-authored-by: Claudia Barcelo <[email protected]>

* comments removed

---------

Co-authored-by: Claudia Barcelo <[email protected]>
* fix tests and typechain + loop

* remove comments

* add back vscode files

* interface fixed
revert PermissionFrozen(_where, _permissionIdOrSelector);
}

if (!_checkFlags(permission.owners[msg.sender], _flags)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

_checkFlags() => _flagIncludes(...)

return;
}

if (_owner != address(1)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Please, add a comment explaining why the address(1) check
  • Use a constant, rather than a literal value
  • Couldn't we just check _newOwner == address(1), update the flags and early return?

BTW, why does address(1) need be be checked here?

  • Why shouldn't we update the counters for 1?

address _where,
bytes32 _permissionIdOrSelector,
address _delegatee,
uint256 _flags
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
uint256 _flags
uint256 _delegateeFlags

/// @return Whether the permission has been created or not.
/// @return How many grant owners this permission has currently.
/// @return How many revoke owners this permission has currently.
function getPermissionData(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function getPermissionData(
function getPermissionStatus(

/// @param _account The address for which to return the current flags.
/// @return uint256 Returns owner flags. 0 if an `account` is not an owner.
/// @return uint256 Returns delegatee flags. 0 if an `account` is not a delegatee.
function getFlags(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function getFlags(
function getPermissionFlags(

return (_permission & _flags) == _flags;
}

/// @notice Checks the permissions for the applyTarget methods used by the plugin setup processor.
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems outdated or unrelated to what the function does

Permission storage _permission,
address _who,
PermissionLib.Operation _operation,
bool isRoot
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Why not calling _isRoot() within the function and saving this parameter? The function is not pure anyway
  • Why isn't the function view?

Comment on lines +19 to +31
// Permission Identifiers necessary to create and set up the dao.
bytes32 constant ROOT_PERMISSION_ID = keccak256("ROOT_PERMISSION");
bytes32 constant APPLY_INSTALLATION_PERMISSION_ID = keccak256("APPLY_INSTALLATION_PERMISSION");
bytes32 constant APPLY_TARGET_PERMISSION_ID = keccak256("APPLY_TARGET_PERMISSION");
bytes32 constant UPGRADE_DAO_PERMISSION_ID = keccak256("UPGRADE_DAO_PERMISSION");
bytes32 constant SET_METADATA_PERMISSION_ID = keccak256("SET_METADATA_PERMISSION");
bytes32 constant REGISTER_STANDARD_CALLBACK_PERMISSION_ID = keccak256(
"REGISTER_STANDARD_CALLBACK_PERMISSION"
);
bytes32 constant SET_TRUSTED_FORWARDER_PERMISSION_ID = keccak256(
"SET_TRUSTED_FORWARDER_PERMISSION"
);

Copy link
Contributor

Choose a reason for hiding this comment

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

Why can't these be read from createdDao.ROOT_PERMISSION_ID()?
We are introducing a double source of truth here

// Permission Identifiers necessary to create and set up the dao.
bytes32 constant ROOT_PERMISSION_ID = keccak256("ROOT_PERMISSION");
bytes32 constant APPLY_INSTALLATION_PERMISSION_ID = keccak256("APPLY_INSTALLATION_PERMISSION");
bytes32 constant APPLY_TARGET_PERMISSION_ID = keccak256("APPLY_TARGET_PERMISSION");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
bytes32 constant APPLY_TARGET_PERMISSION_ID = keccak256("APPLY_TARGET_PERMISSION");
bytes32 constant APPLY_PERMISSIONS_PERMISSION_ID = keccak256("APPLY_PERMISSIONS_PERMISSION");

Comment on lines +273 to +278
/// @inheritdoc IExecutor
function execute(
bytes32 _callId,
Action[] calldata _actions,
uint256 _allowFailureMap
)
external
override
nonReentrant
auth(EXECUTE_PERMISSION_ID)
returns (bytes[] memory execResults, uint256 failureMap)
{
) external override nonReentrant returns (bytes[] memory execResults, uint256 failureMap) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggesting a more readable implementation

    function transfer(
        address to,
        uint value
    ) public auth(ETH_TRANSFER_PERMISSION_ID) returns (bool success) {
        // Can be called directly or via execute()
        // If you call via execute(), you'll need EXECUTE_PERMISSION as well

        if (value == 0) return true;

        (success) = to.call{value: value}();
    }

    function execute(
        bytes32 _callId,
        IDAO.Action[] calldata _actions,
        uint256 _allowFailureMap
    )
        external
        override
        nonReentrant
        auth(EXECUTE_PERMISSION)
        returns (bytes[] memory execResults, uint256 failureMap)
    {
        if (_actions.length > MAX_ACTIONS) {
            revert TooManyActions();
        }

        uint256 gasBefore;
        uint256 gasAfter;
        bool hasExecutePermission = isGranted(
            address(this),
            msg.sender,
            EXECUTE_PERMISSION_ID,
            msg.data
        );

        execResults = new bytes[](_actions.length);

        for (uint256 i = 0; i < _actions.length; ) {
            Action calldata action = _actions[i];

            bool success;
            bytes memory execResult;

            // If action.data is 0 length, it's a native ETH transfer
            if (action.data.length == 0) {
                gasBefore = gasleft();

                // msg.sender will be checked against auth(ETH_TRANSFER_PERMISSION_ID) there
                success = transfer(action.to, action.value);
                execResult = bytes("");

                gasAfter = gasleft();
            } else {
                // Action call
                bool canExecute;
                bytes32 permissionIdOrSelector;

                bytes32 actionSelector = keccak256(action.data[:4]);
                (bool permissionCreated, , ) = getPermissionStatus(action.to, actionSelector);

                if (permissionCreated) {
		                // new permission system
                    canExecute = isGranted(action.to, msg.sender, actionSelector, action.data);
                    permissionIdOrSelector = actionSelector;
                } else {
		                // classic permission system
                    canExecute = hasExecutePermission;
                    permissionIdOrSelector = EXECUTE_PERMISSION_ID;
                }

                if (!canExecute) {
                    revert Unauthorized(action.to, msg.sender, permissionIdOrSelector);
                }

                // Try as a direct call
                gasBefore = gasleft();

                (success, execResult) = action.to.call{value: action.value}(action.data);

                gasAfter = gasleft();

                // Retry as a delegate call
                if (!success && action.to == address(this)) {
                    bytes4 result;

                    assembly {
                        result := mload(add(execResult, 32))
                    }

                    if (result == Unauthorized.selector || result == UnauthorizedOwner.selector) {
                        gasBefore = gasleft();
                        (success, execResult) = action.to.delegatecall(action.data);
                        gasAfter = gasleft();
                    }
                }
            }

            // Track the action result

            if (!success) {
                // Check if failure is allowed
                if (!hasBit(_allowFailureMap, uint8(i))) {
                    revert ActionFailed(i);
                }

                // Make sure that the action call did not fail because 63/64 of `gasleft()` was insufficient to execute the external call `.to.call` (see [ERC-150](https://eips.ethereum.org/EIPS/eip-150)).
                // In specific scenarios, i.e. proposal execution where the last action in the action array is allowed to fail, the account calling `execute` could force-fail this action by setting a gas limit
                // where 63/64 is insufficient causing the `.to.call` to fail, but where the remaining 1/64 gas are sufficient to successfully finish the `execute` call.
                if (gasAfter < gasBefore / 64) {
                    revert InsufficientGas();
                }

                // Store that this action failed.
                failureMap = flipBit(failureMap, uint8(i));
            }

            execResults[i] = execResult;

            unchecked {
                ++i;
            }
        }

        emit Executed({
            actor: msg.sender,
            callId: _callId,
            actions: _actions,
            allowFailureMap: _allowFailureMap,
            failureMap: failureMap,
            execResults: execResults
        });
    }

* some changes

* renaming

* renaming
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants