Building blockchain-based applications enables the creation of software that is executed in a decentralized, trustless, transparent and tamper-proof environment. However, those advantages do not come for free - constructing the decentralized Web presents developers with some unique challenges. In this blog series, engineers at Autark will share their experiences while developing applications for Aragon and lessons learned in the process, as well as some interesting approaches in software development in general.

Aragon leads the the Web3 movement in user-centered, robust design. We at Autark are thrilled to advance the community's position with both the products we release, and enhancements we propose to aragonSDK. One feature that we're working on right now is the forwarderAPI. Forwarding is a feature of aragonOS and a brief exploration of forwarding will highlight the need for the forwarderAPI.

Forwarding: What is It?

Forwarding is what allows for the modular interactions between Aragon apps. Only want specific people to withdraw funds from your Vault? Grant withdrawal permission to the Tokens app for the Executive or Board Token. Want members to vote on withdrawals instead? Grant withdrawal permission to a Voting app. The withdrawal then only executes if the vote passes.

For a more complex example, you might want to create a payout using the allocations app, and let members use Dot Voting on how the funds should be apportioned for a given budget. However, you also might want only token holders to initiate the Dot Vote. This would require the action to first be forwarded to the Tokens app to ensure the creator of the Payout is a token holder, then the action would be sent on to the Dot Voting app, where other token holders can cast their votes.

This level of modularity allows for many interesting decision pathways and tailored governance models. 1Hive has built several useful applications that allow DAOs to lock tokens and implement no-actions delays in their decision pathways, for example.

How forwarding works

Because security is at the forefront of all Web3 technology, forwarding lives inside the aragonOS contract suite. The web frontend (the client) helps to abstract away forwarding so the user only needs to think about what they want to do, and not worry about how they can do it. If a forwarding pathway exists for a desired action, the client maps it out for the user, and the user approves it and submits the transaction generated by the client. But what's happening under the hood?

Example: Finance withdrawals going to a vote

Let's look at an example where a withdrawal from the Finance app is permissioned only through the Voting app. In the client, a user will want to trigger the "New Transfer" action, which will withdraw from the Vault after the vote passes. When they submit the withdrawal, the client will go through the following steps:

  1. Check the ACL (Access Control List) and see that the user doesn't have direct permission to withdraw the money, but the Voting app has permission to do so.
  2. Generate the EVM bytecode (called a CallsScript, or simply "script" sometimes in aragonOS vocabulary) that will withdraw the specified funds to the address entered.
  3. Pass this bytecode to the Voting app, which is configured to create a new vote whenever it recieves a new forwarded action. This new vote instance in the contract will also contain the the CallsScript, which can be thought of as the pending action to be voted on. If the vote passes, the CallsScript will be executed, and the Finance contract will distribute the funds. If the Vote fails, the CallsScript will not be executed and nothing further will occur.

This is the underlying magic that allows actions to be taken directly through decision-making in Aragon! But did you notice something interesting? The Finance withdrawal CallsScript isn't executed at all until the vote passes! This means that the Finance app itself has no way to know that it may or not be withdrawing these funds at the end of a pending vote.

Any other member of the DAO, or third party user who browses to their Finance app will not be aware that the process for this withdrawal has begun* unless they think to check the Voting app. This adds to the learning curve for new users and can create confusion if a DAO updates its decision-making pathways.

forwarderAPI: How apps proactively communicate

The forwarderAPI will allow Aragon apps to have knowledge of pending actions, and share that information with the user in a variety of ways. A forwarder (the app that handles forwarded actions) will be able to inform a target (the app whose action is pending with the forwarder) before the action is executed.

Let's look at how the forwarderAPI can be applied to our in-depth example with the Finance withdrawal going to a vote. In this example, the target is the Finance app and the forwarder is Voting app. The forwarderAPI will allow the Voting app to notify Finance that it has received a pending withdrawal. It can share the details of the withdrawal by passing back the generated radspec string, and it can also keep the Finance app updated regarding the vote's status and even possibly results.

The endpoints

There will be two endpoints. The code is still under review, so their names are subject to change, but one endpoint will be specifically for a forwarder to update the status of a forwarded action from a target app, and then the other will allow apps to share metadata for any purpose with any other apps.

The forwardedAction endpoint

This endpoint will be the pathway for forwarders to let target apps know they've received a forwarded action, and also keep the target updated on its status. In our example above, if the vote fails, the pending withdrawal could be updated to be a failed withdrawal. If the vote passes, the status can be updated to be successful, pending execution, so a user will know to execute the vote. The withdrawal can updated upon execution as well, so it no longer displays the "pending" instance once the actual contract state is updated via the vote's execution.

The metadataRegistry endpoint

This endpoint is designed for forwarders to share details about the state of the forwarded action. In our example, the Voting app can share the latest vote results and/or a deep link to the vote itself with the target app.

Because most apps generate state that might be of interest to other possible apps such as dash boards and activity feeds, apps can publish metadata that is visible to any or all apps within a DAO.

Code example: Dot Voting

Let's go through a quick code example to show how Dot Voting might utilize these endpoints. In Dot Voting, the script.js file that parses contract events and new contract state can include a handler explicitly for calling the forwarderAPI endpoints, called handleForwardedAction:

export const handleForwardedAction = async ({ votes }, { blockNumber, returnValues }) => {
  const { voteId } = returnValues
  const voteIndex = votes.findIndex(vote => vote.voteId === voteId)
  if (voteIndex === -1) return
	const { open, executed, canExecute, executionScript } = votes[voteIndex].data
  
  if (!(open || executed || canExecute )) {
    const voteDataHash = await ipfsAdd({ ...votes[voteIndex].data, source: 'Dot Voting' })
    app.registerAppMetadata(blockNumber, voteId, voteDataHash)
    app.updateForwardedAction(voteId, blockNumber, executionScript, 'failed')
  } else if (executed) {
    app.updateForwardedAction(voteId, blockNumber, executionScript, 'completed')
  } else {
    const voteDataHash = await ipfsAdd({ ...votes[voteIndex].data, source: 'Dot Voting' })
    app.registerAppMetadata(blockNumber, voteId, voteDataHash)
    app.newForwardedAction(voteId, blockNumber, executionScript)
  }
}

The first block of code queries the app state to find the state of the vote of interest. The second block, the if-else block, is where the endpoints (or hooks), communicate that information as needed to the target app. There are three branches. The first branch is entered if the vote state indicates that the action won't ever execute. The second branch is entered if the vote was executed, and the last branch is entered if the vote is still open or not yet executed.

Note that the branch where the vote has executed solely calls updates the updateForwardedAction hook. This is because an executed action cannot yet be easily mapped to a pending action's published metadata. There are a few potential ways to do this that under exploration, but will not be part of the initial release.

In this example, Dot Voting is publishing the vote details such as vote start time, duration and current totals for each vote to the registerAppMetadata hook. Note that the metadata hook excepts the block number of the contract event triggering this action, blockNumber, the unique ID of the vote, voteId, and an IPFS hash of the vote details, the voteDataHash.  

The blockNumber is used by the client to determine whether or not to add this action to the persistent data cache. The voteId serves as a key in the metadata register, and the voteDataHash is the actual payload. There is an optional fourth parameter, where specific apps can be designated as recipients of the metadata payload. If left blank, this metadata is accessible to all apps.

The newForwardedAction hook and updateForwardedAction hook call the same json-rpc command under the hood and might be combined into a setForwardedAction hook, with the status as a the optional last parameter.

Code example: Allocations

Because the forwarderAPI shares forwarder app state with one or more apps, we figured what better way to quickly disseminate the details of a new forwarded action than through a synthetic contract event? With the introduction of the forwarderAPI, a new synthetic event can be passed into the store called ForwardedActions. Any target app can set up a handler for this event in the app state reducer in order to render forwarded actions in the frontend. Below is an example event handler written for the Allocations app:

const onForwardedActions = async ({ failedActionKeys = [], pendingActionKeys = [], actions }) => {
  const offchainActions = { pendingActions: [], failedActions: [] }

  const getDataFromKey = async key => {
    const action = actions[key]
    const data = await app.queryAppMetadata(action.currentApp, action.actionId).toPromise()
    if (!data) return
    let metadata = await ipfsGet(data.cid)
    if (!metadata) return
    return { ...action, ...metadata }
  }

  let getFailedActionData = failedActionKeys.map(getDataFromKey)

  let getPendingActionData = pendingActionKeys.map(getDataFromKey)

  offchainActions.failedActions = (await Promise.all(getFailedActionData))
    .filter(action => action !== undefined)
    .map(action => ({ ...action, 
      startTime: new Date(action.startDate), 
      description: action.metadata, 
      amount: String(action.balance),
      distSet: false,
      pending: false
    }))

  offchainActions.pendingActions = (await Promise.all(getPendingActionData))
    .filter(action => action !== undefined)
    .map(action => ({ ...action, 
      startTime: new Date(action.startDate), 
      description: action.metadata, 
      amount: String(action.balance),
      distSet: false,
      pending: true
    }))

  return offchainActions
}

First, under the hood, aragonAPI has sorted forwarded actions into failed and pending action keys and all actions are available as a key-value store. Once an actionId is found, it, along with the forwarder are used to find the metadata. The current app's address and the actionId serve as a primary key for retrieving metadata. The remainder of the handler is dedicated to passing each failed and pending action key through the getDataFromKey function to call queryAppMetadata and retrieve the most up to date state from the forwarder and shaping it for consumption by the frontend.

Why aren't executed actions handled? This is because that state is already directly accessible from the target app's contract. However, the completed actions are still available in the provided actions key-value store.

For a bit more context, the above function is called in reducer like so:

export const handleEvent = async (state, event, settings) => {
  const { address: eventAddress, event: eventName, returnValues } = event
  const { addressBook, vault } = settings
  const { accounts, entries, payouts } = state

  let nextState = { ...state }
  switch (eventName) {
    // other cases
    
    case 'ForwardedActions':
        nextState.offchainActions = await onForwardedActions(returnValues)
	    break
        
    // other cases
	}
 
  return nextState
}

In essence, the metadata registry endpoints act as a data store, and the updateForwardedAction endpoints act as hooks that signal to target apps that there is pending activity, and to check the metadata registry for further details.

Summary

We're excited about the forwarderAPI and how it will help users to more intuitively grasp how information flows through their DAOs. Because this feature is still under development, please reach out to us on Keybase or Aragon chat with any questions or feedback. Of course, this post will be updated once the feature is finalized, along with a link to the documentation.