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.
- 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)
transfer: moves tokens out of the Actor app. Protected by
deposit: pulls tokens from
msg.senderto the Actor app.
2. Arbitrary call execution
Executes an arbitrary call from the Agent app to a user inputed address.
function execute(address target, uint256 ethValue, bytes data) authP(EXECUTE_ROLE, arr(target, ethValue, extractSignature(data))) external
- Perform an EVM
callsending 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.
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)
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:
ethValueis positive: data must be non-empty and target code size be greater than 0. For vanilla ETH transfers, the
transferfunction 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
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 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
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.
ecrecover(hash, sig, sig[0:31], sig[32:63])equals to the designated signer address, return
- Otherwise, return
falseunless 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
- If it returns 32 bytes of data equal representing a 1, return
- If the call reverts or returns false, return
falseunless 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 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.