crvUSD Ape Tutorial 4: Sophisticated Voter
Inductive derivation to understand Curve Aragon DAO voting
Our fourth major tutorial for our Ape Framework tutorial in particular benefits from a full writeup, as it involves an inductive process to step through decoding a Curve DAO vote via the Aragon contracts.
In particular, we’re hoping to elevate everybody to the “sophisticated voter” stage, as Curve occasionally asks for help from this elite class to “verify” DAO votes.
For the tutorial, we consider DAO vote 573.
Note the UI indicates this is the 651st overall vote, but within this particular sequence it’s vote number 573, which you can glean from the URL https://dao.curve.fi/vote/ownership/573
The UI includes a “description,” but this “description” is whatever gets uploaded… you could write a description that says “do something good” and then have the actual vote do something malicious if you do not verify the votes.
The Convex UI shows a bit more details derived from the actual instructions
Still, unless you can convert the calldata in your head, you’ll need to do a few more steps to properly verify this vote.
As always, the most helpful resource to start with is the technical Curve Docs.
From here we can grab the address on Etherscan for the ownership contract, where a lot of votes happen for the very active Curve DAO.
You can use the “Read as Proxy” function to read all the information about the vote.
The bytes data in the `script` portion of the response is the actual instructions that voters are authorizing. It looks quite complex!
0x0000000140907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd0000000000000000000000000000000000000000000000000000000040907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
In fact, you can look through it and find patters. Break it down analytically… the first part…
0x00000001
…40907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd0000000000000000000000000000000000000000000000000000000040907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
This doesn’t look so bad…. the first 4 bytes (8 characters) are just zeroes ending in a 1.
If you loop through every vote, they all carry this same first four bytes. You can presume this is a constant and treat it as an opening prefix for every Aragon vote to this contract.
Looking at the original bytecode, after this prefix, there’s a lot of non-zero bytes for the next 40 characters.
PREFIX 0x00000001
…
40907540d8a6c65c637785e8f8b742ae6b0b9968
…
000000a4b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd0000000000000000000000000000000000000000000000000000000040907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
Eyeballing it, you might think this chunk of data resembles an Ethereum address. Sure enough, this exact set of characters shows up right in the docs as the Aragon agent.
Suddenly our bytecode is becoming comprehensible. Let’s grab the next set of zeroes…
PREFIX 00000001
TARGET 40907540d8a6c65c637785e8f8b742ae6b0b9968
…
000000a4
…
b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd0000000000000000000000000000000000000000000000000000000040907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
This part is common to “structured data formats.” You need a means for a parser or program to procedurally unpack the data. So far we’ve dealt with “fixed length” data that’s always exactly the same size… the prefix will always be four bytes, the target Ethereum address will always b3 bytes.
But the instructions might be “variable length”. In this case it is short, we’re calling a very simply function to write parameters with no additional data. But it may be really long if you need to encode a list of parameters.
If you need to store variable length data, which could be 5 bytes or 5000 bytes, you need to tell your parser how many characters to chunk off next. So you include a fixed length piece of data saying how many bytes are next.
Reserving the next 4 bytes to declare the length, it makes a lot of sense it has a lot of zeroes, since this is small. You can use a native python function to convert this to an integer:
int(bytes_data.hex(), 16)
This gives us 164. So we can bite off the next 164 bytes
PREFIX 00000001
TARGET 40907540d8a6c65c637785e8f8b742ae6b0b9968
LENGTH 000000a4
…
b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
…40907540d8a6c65c637785e8f8b742ae6b0b9968000000a4b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
Here we have a long string of bytes data. We don’t have much hope of decoding it, but you can use Ape Framework (or previously Brownie) to decode this input using the decode_input()
function native to any Contract call. This function matches the input to the ABI and decodes it into readable data.
The instructions are successfully decoded here, in that the bytecode is triggering the execute
function on the Aragon agent.
This in turn has its own set of bytecode that can be decoded…
So it all calls the apply_new_parameters()
function on 0xf5f5…e284, aka TriCryptoUSDT.
The process can then be repeated to decode the remainder of the bytecode. It ends up as follows
PREFIX 00000001
TARGET 40907540d8a6c65c637785e8f8b742ae6b0b9968
LENGTH 000000a4
CALLDATA b61d27f6000000000000000000000000f5f5b97624542d72a9e06f04804bf81baa15e2b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
TARGET2 40907540d8a6c65c637785e8f8b742ae6b0b9968
LENGTH2 000000a4
CALLDATA2
b61d27f60000000000000000000000002889302a794da87fbf1d6db415c1492194663d130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042a7dd7cd00000000000000000000000000000000000000000000000000000000
Which decodes as the same call, but to a different contract
As you can see, the bytecode looks scary, but it’s quite comprehensible when you break it down. We also talk through this entire process in the companion video.
We’ve included a companion Ape Framework script you can use to decode any historical or future Curve vote.
Big thanks to mo_anon for the outstanding documentation and Wormhole Oracle for the original script in Brownie.