Skip to main content

Encoding Transaction Data

It is our belief that most of the power of the DAO lies in its autonomy. Pilots can vote on proposals that perform actions, and when those proposals pass, they can trigger the actions immediately and trustlessly, on behalf of the DAO. In order for the DAO to understand a proposal's associated actions, they must be encoded as transaction data, and this data must be included at the time of the proposal's creation. This article explains the process of encoding and submitting transaction data for proposals.

Selecting Transaction Data

The majority of transactions you'll want to attach will either send tokens directly to an address, or will make some sort of call to a contract. As it turns out, they're both very similar. At the time of writing, all transactions that support attachment have the following shape:

  • target - This is the address of the wallet to which you're sending ether, or the address of the contract that the transaction will call.
  • value - This is the number of wei to send to the target during the transaction. On Polygon, this is MATIC wei, and on Ethereum, it is ETH wei. On both networks, a full token (MATIC/ETH) consists of 101810^{18} wei.
  • calldata - This data is included so that the target knows how to process your transactions. For contract calls, calldata selects the function that you are calling and the inputs that you are passing to it. For direct token transfers, calldata may be either 00, or a UTF-8 encoded note to the recipient.

Encoding Calldata

We'll focus on encoding calldata for contract calls, since it is effectively optional in direct transfers. For contract calls, calldata consists of the following items, packed together through Solidity's non-standard packed encoding:

  • signature - This is a 4-byte number that identifies the function and overload that your transaction will call. You can find a function's signature by copying its canonical signature (formatted name and parameters), hashing it with keccak256\operatorname{keccak256}, and then taking the top 4 bytes of the hash.
  • parameters - This consists of all of the inputs that your transaction will pass to the contract function, encoded under Solidity's standard ABI encoding.

As an example, let's encode a transfer of 5 WETH from the timelock to Vitalik.

First, we look up the WETH contract on Polygon and take a look at its transfer function. We can see that its canonical signature is transfer(address,uint256).

We can then compute its signature:

signature=keccak256("transfer(address,uint256)")[0:4]\text{signature} = \operatorname{keccak256}(\text{"transfer(address,uint256)"})[0:4]

signature=(0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b)[0:4]\text{signature} = (\text{0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b})[0:4]

signature=0xa9059cbb\text{signature} = \text{0xa9059cbb}

To compute our parameters, we first find their native representations. This means Vitalik's hex address for the recipient and 5 WETH's worth of wei for the amount.

  • Vitalik's address - 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\text{0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045}
  • 5 WETH in wei - 50000000000000000005000000000000000000

We can then use Solidity's ABI encoding to put them together:

parameters=abi.encode(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 5000000000000000000)\text{parameters} = \operatorname{abi.encode}(\text{0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 5000000000000000000})

parameters=0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000004563918244f40000\text{parameters} = \text{0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000004563918244f40000}

Putting them together, with the signature at the beginning, we get:

calldata=0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000004563918244f40000\text{calldata} = \text{0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000004563918244f40000}

Including Transaction Data in Proposals

Once you have selected and encoded your transaction data, including it within a proposal is fairly straightforward. To create any proposal, with or without transaction data, you call the propose function on the governor contract:

function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) {}

For proposals without transactions, you pass empty arrays for all parameters except for description, which you would fill with text describing your proposal (ideally, in line with the best practices we've outlined).

The only change we make to include transactions is filling the targets, values, and calldatas arrays with the targets, values, and calldatas for each of our transactions, in order. In the case that one transaction does not need a value or has no calldata, we do not omit its value or calldata from the list. Instead, we use 00.

Let's revisit our example of sending Vitalik 5 WETH. Using our calldata from earlier, we can create our proposal by calling

propose(
// The address of the WETH contract on Polygon
[0x7ceb23fd6bc0add59e62ac25578270cff1b9f619],
// We're requesting a transfer of an ERC20 token, not sending MATIC directly, so we pass 0
[0],
// Our calldata from the previous section
[0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000004563918244f40000],
// This is the text portion of the proposal that gets shown to voters
"Vitalik needs 5 WETH ASAP"
);