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
)
Functions:
-
transfer
: moves tokens out of the Actor app. Protected byTRANSFER_TOKENS_ROLE
-
deposit
: pulls tokens frommsg.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. - 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, thetransfer
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, returntrue
- 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.