Writing custom permission conditions

Permission conditions relay the decision if an authorized call is permitted to another contract. This contract must inherit from PermissionCondition and implement the IPermissionCondition interface.

interface IPermissionCondition {
  /// @notice This method is used to check if a call is permitted.
  /// @param _where The address of the target contract.
  /// @param _who The address (EOA or contract) for which the permissions are checked.
  /// @param _permissionId The permission identifier.
  /// @param _data Optional data passed to the `PermissionCondition` implementation.
  /// @return allowed Returns true if the call is permitted.
  function isGranted(
    address _where,
    address _who,
    bytes32 _permissionId,
    bytes calldata _data
  ) external view returns (bool allowed);
}

By implementing the isGranted function, any number of custom conditions can be added to the permission. These conditions can be based on

  • The specific properties of

    • The caller who

    • The target where

  • The calldata _data of the function such as

    • Function signature

    • Parameter values

  • General on-chain data such as

    • Timestamps

    • Token ownership

    • Entries in curated registries

  • Off-chain data being made available through oracle services (e.g., [chain.link](https://chain.link/), [witnet.io](https://witnet.io/)) such as

    • Market data

    • Weather data

    • Scientific data

    • Sports data

The following examples illustrate.

Examples

The following code examples serve educational purposes and are not intended to be used in production.

Let’s assume we have an Example contract managed by a DAO _dao containing a sendCoins function allowing you to send an _amount to an address _to and being permissioned through the auth modifier:

contract Example is Plugin {
  constructor(IDAO _dao) Plugin(_dao) {}

  function sendCoins(address _to, uint256 _amount) external auth(SEND_COINS_PERMISSION_ID) {
    // logic to send `_amount` coins to the address `_to`...
  }
}

Let’s assume you own the private key to address 0x123456…​ and the Example contract was deployed to address 0xabcdef…​. Now, to be able to call the sendCoins function, you need to grant the SEND_COINS_PERMISSION_ID permission to your wallet address (_who=0x123456…​) for the Example contract (_where=0xabcdef…​). If this is the case, the function call will succeed, otherwise it will revert.

We can now add additional constraints to it by using the grantWithCondition function. Below, we show four exemplary conditions for different 4 different use cases that we could attach to the permission.

Condition 1: Adding Parameter Constraints

Let’s imagine that we want to make sure that _amount is not more than 1 ETH without changing the code of Example contract.

We can realize this requirement by deploying a ParameterConstraintCondition condition.

contract ParameterConstraintCondition is PermissionCondition {
  uint256 internal maxValue;

  constructor(uint256 _maxValue) {
    maxValue = _maxValue;
  }

  function isGranted(
    address _where,
    address _who,
    bytes32 _permissionId,
    bytes calldata _data
  ) external view returns (bool) {
    (_where, _who, _permissionId); // Prevent compiler warnings resulting from unused arguments.

    (address _to, uint256 _amount) = abi.decode(_data, (address, uint256));

    return _amount <= _maxValue;
  }
}

Now, after granting the SEND_COINS_PERMISSION_ID permission to _where and _who via the grantWithCondition function and pointing to the ParameterConstraintCondition condition contract, the _who address can only call the sendCoins of the Example contract deployed at address _where successfully if _amount is not larger than _maxValue stored in the condition contract.

Condition 2: Delaying a Call With a Timestamp

In another use-case, we might want to make sure that the sendCoins can only be called after a certain date. This would look as following:

contract TimeCondition is PermissionCondition {
  uint256 internal date;

  constructor(uint256 _date) {
    date = _date;
  }

  function isGranted(
    address _where,
    address _who,
    bytes32 _permissionId,
    bytes calldata _data
  ) external view returns (bool) {
    (_where, _who, _permissionId, _data); // Prevent compiler warnings resulting from unused arguments

    return block.timestamp > date;
  }
}

Here, the permission condition will only allow the call the _date specified in the constructor has passed.

Condition 3: Using Curated Registries

In another use-case, we might want to make sure that the sendCoins function can only be called by real humans to prevent sybil attacks. For this, let’s say we use the Proof of Humanity (PoH) registry providing a curated list of humans:

interface IProofOfHumanity {
  function isRegistered(address _submissionID) external view returns (bool);
}

contract ProofOfHumanityCondition is PermissionCondition {
  IProofOfHumanity internal registry;

  constructor(IProofOfHumanity _registry) {
    registry = _registry;
  }

  function isGranted(
    address _where,
    address _who,
    bytes32 _permissionId,
    bytes calldata _data
  ) external view returns (bool) {
    (_where, _permissionId, _data); // Prevent compiler warnings resulting from unused arguments

    return registry.isRegistered(_who);
  }
}

Here, the permission condition will only allow the call if the PoH registry confirms that the _who address is registered and belongs to a real human.

Condition 4: Using a Price Oracle

In another use-case, we might want to make sure that the sendCoins function can only be called if the ETH price in USD is above a certain threshold:

import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';

contract PriceOracleCondition is PermissionCondition {
  AggregatorV3Interface internal priceFeed;

  // Network: Goerli
  // Aggregator: ETH/USD
  // Address: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e
  constructor() {
    priceFeed = AggregatorV3Interface(
      0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e
    );
  }

  function isGranted(
    address _where,
    address _who,
    bytes32 _permissionId,
    bytes calldata _data
  ) external view returns (bool) {
    (_where, _who, _permissionId, _data); // Prevent compiler warnings resulting from unused arguments

    (
      /*uint80 roundID*/,
      int256 price,
      /*uint startedAt*/,
      /*uint timeStamp*/,
      /*uint80 answeredInRound*/
    ) = priceFeed.latestRoundData();

    return price > 9000 * 10**18; // It's over 9000!
  }
}

/* Here, we use https://docs.chain.link/docs/data-feeds/ providing us with the latest ETH/USD price on
the Goerli testnet and require that the call is only allowed if the ETH price is over $9000. */