EIP-644: A standard for permission token

Abstract

A new token standard that held the permission of an address in an ecosystem. A permission token can be transferred by the owner and can be granted/revoked to an authorized operator.

Motivation

We still need implement and manage smart contract by permissioned addresses that’s why you may see special roles like Owner, Operator… These permissions aren’t managed correctly and it’s really hard if we want to transfer, grant, revoke permissions.

Let’s check the Ownable.sol

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _transferOwnership(_msgSender());
    }

    function owner() public view virtual returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

This contract is only able to provide basic permission for an owner, define another role or permission is expensive and it’s hard to mange the relationship between smart contracts.

Specification

This token shares the same interface with other standard:

interface PermissionToken {
    function name() external returns(string memory);
    function symbol() external returns(string memory);
    function transfer(address to, uint256 permission) external returns(bool);
    function balanceOf(address owner) external view returns(uint256)
}

Note: I don’t think permission allowance is a good idea but let’s discuss later.

Storage

A mapping can be used to store 256 bits of permissions of an address:

mapping (address => uint256) balances;

We can define a permission is a power of 2:

uint256 constant PERMISSION_NONE = 0;
uint256 constant PERMISSION_CREATE = 1;
uint256 constant PERMISSION_SIGN = 2;
uint256 constant PERMISSION_EXECUTE = 4;

To check an address have some certain permissions, we just need to do a simple trick by using bitmask check.

Checking

Define a role:

uint256 constant ROLE_ADMIN = PERMISSION_CREATE | PERMISSION_SIGN | PERMISSION_EXECUTE;

Check role and permission:

modifier onlyAllow(uint256 permission) {
    require(permissionToken.balanceOf(msg.sender) & permission > 0, 'Access Denied');
    _;
}

function upgradeContract() onlyAllow(ROLE_ADMIN) {
    // some special code
}

Transfer permissions

Transfer method allows an owner to transfer a subset of their permissions to another address.

function transfer(address to, uint256 permissions) external returns(bool) {
    require(balances[msg.sender] & permissions > 0);
    require(address(to) != address(0));
    // Remove permissions from sender
    balances[msg.sender] =  (balances[msg.sender] | permissions) ^ permissions;
    // Assign permissions to receiver
    balances[to] = balances[to] | permissions;
    return balances[to] & permissions > 0;
}

Delegate permissions

Allow an authorized operator to trigger smart contract for behalf of permission owner.

Note: Authorized operator can not transfer delegated permissions.

Conclusion

  • This approach could help reduce security fault regarding to permission management
  • This proposal simplifies management process and it is extendable in the future (apparently you can define 256 permissions and 2^256 roles).
  • This standard can be used to secure the interaction between two different ecosystems.
  • An owner can grant a subset of their permission to a given address and they can revoke the access anytime.

I just quickly glanced over this proposal, but wouldn’t it be a common desire that the entity granted permission to perform an action is not the same as the entity in control for choosing who holds this permission?

For example, a smart contract may have permission to do some basic actions, but a separate election/governance smart contract would be in control of changing ownership of this permission.

This is crucial for separation of concerns, and provides increased security when we separate these requirements into different smart contracts.

How can this be achieved using your proposal?

Perhaps the implementation itself could have some sets of permissions for transfer etc. which are then controlled by other permission tokens.

1 Like

The entity perform an action can be different from the entity in control of these permissions. This proposal allow permissions to be managed (transfer/grant/revoke) in a much more flexible and secure manner. In the reality, a DAO contract SHOULD hold the SUPER_USER_ROLE and grant a subset of its permission to any authorized operator via a weighted voting process.

mapping (address => uint256) delegatedPermission;

function grant(address operator, uint256 permissions) {
   require(balances[msg.sender] & permissions > 0, 'Access Denied');
   delegated[operator] = delegated[operator] | permissions;
}

function revoke(address operator, uint256 permissions) {
   require(balances[operator] & permission > 0, 'Access Denied');
   delegated[operator] = (delegated[operator]  | permissions) ^ permissions;
} 

E.g: permissionToken.grant(chirdDAO, PERMISSION_CREATE | PERMISSION_EXECUTE)

We have 256 bits to store a given role (it can represent for 256 different permissions). This approach is flexible to adapt with new changes, we can introduce new permissions without breaking anything.

contract PermissionV1 {
    uint256 private constant PERMISSION_TRANSFER = 2**0;
    uint256 private constant PERMISSION_DELEGATE = 2**1;
    uint256 private constant PERMISSION_UPGRADE = 2**2;
}

contract EcosystemPermissionTokenV1 is PermissionToken, PermissionV1 {
    function transfer(address to, uint256 permission) external onlyAllow(PERMISSION_TRANSFER);
    function grant(address operator, uint256 permission) external onlyAllow(PERMISSION_DELEGATE);
}

contract PermissionV2 is PermissionV1 {
    uint256 private constant PERMISSION_CREATE = 2**3;
    uint256 private constant PERMISSION_EXECUTE = 2**4;
    uint256 private constant PERMISSION_SIGN = 2**5;
}

contract EcosystemV2 is PermissionV2 {
     // Permission V1 and V2 can be verified
     // without any changes from PermissionTokenV1
}

We can combine more than one permission tokens if needed.

What I mean is that the governance process should have permission to transfer permission rather than the contract that bears the permission, e.g. the transfer method might look something like this:

function transfer(address to, uint256 permissions) onlyAllow(ROLE_MANAGE_PERMISSIONS) ...

Delegating permissions is good here because it prevents the contract using these permissions from accidentally changing ownership. However, theoretically, the governance process which manages the ownership of the permissions could accidentally invoke the smart contract, which is perhaps a security risk. Of course, if it didn’t have this ability, it could just give itself permission, but at least this extra step would reduce the likelihood of this occurring.

1 Like

Noted, thank you. This proposal will focus on how we organize and manage permissions and roles. So we need to draw a line between the implementation and the standard.

1 Like