Builds on the discussions in this thread Dynamic Permissions for Organization "Actions" with Signer Integration, creating a new topic to focus the conversation on this new spec.

WIP implementation: https://github.com/aragon/aragon-apps/pull/580

Aragon Agent app

The Agent app is a superset of the Vault app. It can hold valuable assets (ETH and ERC20) and perform external actions. In many instances the Agent app will be the external interface of the DAO, as it can perform actions on her behalf.

The Agent app may end up replacing the Vault app as the default app where assets are held in a DAO. However, a more conservative rollout is proposed having both apps available and allow DAOs to migrate whenever they decide to do so.

The Agent app builds on top jvluso’s Identity app and generalizes it to fit more use cases.

User stories

An Aragon DAO can interact with other Ethereum smart contracts or protocols without the need of implementing a custom Aragon app for every protocol.

A user/member of a DAO can use any Ethereum dApp identified as their DAO, signer integrations can take care of routing the intent through the governance process of the DAO.

An Aragon DAO can participate as a stakeholder in another DAO, allowing the creation of DAO stacks.

Contract specification v1

1. Vault inheritance ( Agent is Vault )

See Vault v4 implementation.

Functions:

transfer : moves tokens out of the Actor app. Protected by TRANSFER_TOKENS_ROLE

: moves tokens out of the Actor app. Protected by deposit : pulls tokens from msg.sender to the Actor app.

2. Arbitrary call execution

Executes an arbitrary call from the Agent app to a user inputed address.

Signature:

function execute(address target, uint256 ethValue, bytes data) authP(EXECUTE_ROLE, arr(target, ethValue, extractSignature(data))) external

Functionality

Perform an EVM call sending all available gas to target, sending the specified ETH amount and calldata.

sending all available gas to target, sending the specified ETH amount and calldata. If the call reverts, revert forwarding the error data as the error data of the main call frame.

If the call succeeds, emit an event logging the arguments the function was called with.

Security

It would be extremely cumbersome to ensure that ‘vanilla’ ETH and ERC20 transfers cannot happen, as they could be masqueraded in many ways (e.g. create a contract that receives ETH regardless of the call data, or send tokens with approve + transferFrom )

The EXECUTE_ROLE should be treated as a super-role of TRANSFER_TOKENS_ROLE , as someone with the role would be able to transfer tokens and also perform additional actions (unless extremely well protected with ACL params).

However we could have the following check, mostly as a sanity check:

If ethValue is positive: data must be non-empty and target code size be greater than 0. For vanilla ETH transfers, the transfer function should be used.

3. Forwarding interface ( Agent is IForwarder )

Making the Agent app a fully-fledged forwarder will ease inter-DAO interactions (DAOs acting in other DAOs) with inter-DAO transaction pathing™️, as well as allow EVMScript execution for the ease of executing more complex actions in one call (even though the Agent will probably be called from a script in most cases).

Executing EVMScripts with the Agent app should require holding the RUN_SCRIPT_ROLE role, which can be parametrized with the keccak256 hash of the script.

The reasons for supporting arbitrary call executions too and not only pure script execution are:

ACL parametrization will be less powerful when executing scripts, as script inspection is virtually impossible.

No existing EVMScript executor supports sending ETH with calls.

Note that granting the RUN_SCRIPT_ROLE is virtually like granting TRANSFER_TOKENS_ROLE but without the possibility of parametrizing permissions, therefore it should be more restricted.

4. Signature handling

Smart contracts addresses don’t derive from private keys, therefore it is impossible for a contract to do an ECDSA signature. However, there are protocols in which users authorize actions using signatures (e.g. making an order in 0x). As the ecosystem moves forward with account abstraction, and the assumption that everyone uses a EOA to interact with contract dies, we can push for a standard way for contracts to ‘sign messages’.

A standard for contracts ‘signing messages’ (already live in 0x v2), is for the contract to expose a isValidSignature function that gets called for verifying whether the contract approves a given signature as its own:

function isValidSignature(bytes32 hash, bytes sig) public view returns (bool)

Note that the function is a view and shouldn’t modify state. We should assume it is always executed with a staticcall .

There are two routes that the Agent app could return true to a isValidSignature call, both of which can (and should) co-exist:

4.1 Designated signer

Protected by the DESIGNATE_SIGNER_ROLE one designated signed for the Agent app can be set.

Function signature:

function setDesignatedSigner(address designatedSigner) authP(DESIGNATE_SIGNER_ROLE, arr(designatedSigner)) external

The designated signer should replace the current designated signer in the contract state and emit an event.

The response to isValidSignature depends on the nature of the designated signer:

4.1.0 Designated signer is not set

Return false unless the hash has been pre-signed (see section 4.2)

4.1.1 Designated signer is an EOA

Checks whether the signature is a valid signature of the hash by the designated signer.

Extract signature components from the data byte array.

If ecrecover(hash, sig[64], sig[0:31], sig[32:63]) equals to the designated signer address, return true

equals to the designated signer address, return Otherwise, return false unless the hash has been pre-signed (see section 4.2)

4.1.2 Designated signer is a contract

Forwards the signature checking to the designated signer. A contract designated signer may implement a different signing algorithm (e.g. the designated signer may check a ring signature).

Perform a staticcall ( designatedSigner.isValidSignature(hash, sig) ).

( ). If it returns 32 bytes of data equal representing a 1, return true

If the call reverts or returns false, return false unless the hash has been pre-signed (see section 4.2)

4.2 Pre-signed hashes

Protected with the PRESIGN_HASH_ROLE , this function allows to mark a hash as presigned, and therefore make the isValidSignature function always return true for that hash.

Function signature:

function presignHash(bytes32 hash) authP(PRESIGN_HASH_ROLE, arr(hash)) external

The hash will be marked as signed in the contract storage, and once marked as signed it should never be reverted as not signed (in the same way that you cannot revoke an ECDSA signature).

An optimized implementation should always check first if the hash had been presigned before checking if the hash is properly signed by the designated signer.