Initial Attempts to Reverse Engineer Belisarius
For an introduction to this series, see here. For quick reference, Belisarius is the nickname I rather arbitrarily gave to the address 0xa57bd00134b2850b2a1c55860c9e9ea100fdd6cf on Ethereum mainnet, which I’ll try to explain another time.
This article’s art was created by Etheria Chan as a part of some explorations in generated images, and guided by a vision of the subject matter as a cyberpunk investigative journey. If you decide to mint this article as an NFT, the revenue will be split 50/50 between Etheria and myself.
The image was cropped to fit Mirror’s format for cover art, if you’d like to see the full original, see here. (Thanks to zkDoof again for the help uploading it.)
Belisarius is a contract. This decision is already interesting to me - I’m not sure what the advantage of using a contract over an EOA bot, but I’ve seen this in other bots too. My first thought was that this seemed disadvantageous since it would seem terrifically restrictive, since contracts should only be able to operate strategies coded into them, and it’s hard to believe they could be expressive and general enough to cover the gamut of strategies that could present themselves. As you will find here, that thought was indeed in vain.
Once upon a time I had a pair programming/learning session weekly with th4s. We had worked on a better understanding of Yul-level assembly and opcodes, and were looking for a new project. A quick glance at the Contract section on Belisarius’s Etherscan page will show that the code is (as one would suspect) unverified, so we decided we’d try and understand what this contract is and how it interacts with the chain. This was the first step in falling down the Belisarius rabbit hole.
As a warning, the telling here is roundabout. We could unceremoniously drop the conclusions here, but in my humble opinion, a lot of the value is in the journey. Perhaps I’m mistaken, but I feel that literature on attempting reverse engineering a contract is scant, and the process as it looked like to a plebeian engineer such as myself may be in and of itself useful. It’s certainly therapeutic for me.
th4s recommended EtherVM as a decompiler. You can see the decompilation for Belisarius here. EtherVM gives a pretty good readout of the functions in the contract at the top, which we’ll get into. You may notice that a surprising number of functions are named for us at the top of the page. If function names aren’t stored in the contract code (they aren’t), how does EtherVM know what the names and arguments are? The answer is that the function selectors are stored in the contract (those are the first 4 bytes of the keccak256 hash of the function signature, and the function signature the name of the function and the argument data types packed, for example “transfer(address,uint256)”
, see this excellent article by Veronica Coutts for more). There is a popular database of function selectors and the function name/arg combo which produces it called 4byte.directory, and EtherVM queries it with the function selectors it detects in contracts. (It might be interesting to dive into how EtherVM detects function selectors, since I have seen contracts where it couldn’t figure out where the selectors were, but I suspect that’ll be a topic for later, maybe when we start diving more into strategy contracts.) Truth is, a bunch of things use 4byte - it’s also how Etherscan identifies which functions are being called, along with MetaMask.
In any event, as mentioned, a bunch of the functions are actually named by 4byte, which is kind of fascinating for a contract which we would expect to be all shadowy-like. A bunch of the names don’t seem too descriptive though; “rely” and “deny” seem like opposites, but what are they doing? What are “wards”? We all know what a cache is, but what is it doing here? I found some answers in a surprising place, but as mentioned, we’re taking the high road here. In truth, there’s a big shortcut to finding out what’s going on over here, but I went the long way, and I figure it might be fun and/or instructive for people if I describe what that long way was, and it’s at least cathartic, so here we go.
execute(address,bytes)
Looking casually through Belisarius’s transactions, we can see that most of them are interactions with the execute
function, or more precisely functions, since execute
is an overloaded function on Belisarius - one takes (address,bytes)
, the other (bytes,bytes)
. (Though the vast majority of calls, or maybe even all of them, I don’t recall, are to the (address,bytes)
variant.) I don’t remember the precise series of events, but I think we decided to try figuring this function out first.
Looking at the decompilation on EtherVM shouldn’t be that unfamiliar if you’ve looked through a contract in Yulp or Huff, but in case you haven’t, we’ll break it down a bit. I want to emphasize “a bit”, since there are a ton of lines here that I don’t particularly understand since at the end of the day I’m simply not all that gigabrained. I’ll just focus on what we did reason about, and somethings I’ve figured out later. The first point of interest is the if/else if flow that starts a few lines in. This is represented in Yulp and others as a switch table, and it’s for figuring out if a call to the contract is interacting with any of the contract’s functions or not. Any call made to a contract which is supposed to call a specific function starts with the function selector (which we explained above), so the switch table simply tries to match the first 4 bytes of the call with all of the function selectors in the contract. Those 4 bytes are represented as var0
in EtherVM here.
Incidentally, you may be wondering why the if/else ifs are nested in another if which checks if 0x78e111f6 > var0
. This is a search optimization. By ordering the functions in order of the hexadecimal of their selectors, even though that would seem irrelevant, calls to functions in the second half of the list (the ones with higher hex values) is reduced. This is a common optimization in many areas of coding, and is called a binary search.
The very first selector is the function we’re interested in - execute(address,bytes)
. I seem to remember our original intuition is that this would be something like multicall, allowing batching together a bunch of calls into one tx. Looking back, it should have been more suspicious that the first argument was address
and not address[]
. I’ll add some intuition I’ve gained from looking at EtherVM. Inside the jump table usually all that I’ve seen so far is slicing the selector from calldata, and then saving the length of the calldata minus the first four bytes (the selector), and then passing them along. There can also be checks to make sure the calldata is long enough to support its arguments. After that, the first jump is generally to datatype validation and processing (but not always). In particular, if there are arrays, this is where they’ll be unpacked. I plan on getting into that more in the sequel to this chapter, where we dive into a function with an unknown selector. After that, EtherVM places in another jump to an arbitrarily named internal function (which isn’t necessarily an internal function in the source code, as far as I can tell). In this case, after the initial check there’s a jump to something EtherVM calls func_0603
.
func_0603
starts by loading msg.sender
and a bunch of zeros into the first 64 (0x40) bytes of memory. If you’ve read up on mappings, you can tell the next line is checking a mapping using msg.sender
as the key, and expecting it to return 1, reverting if it doesn’t. This is a basic auth check using a mapping as a whitelist of addresses that can call functions like this. I think understanding this was around when I felt like a 1337 Hackerman.
Of course then Belisarius made me cry by throwing a delegatecall
at me. Those have a bunch of arguments, and the memory slicing in the decompiler made my eyes water. It took us a while to realize what was going on here. We found it a bit easier to use Etherscan to figure out what was happening. Looking at some txs there combined with the decompilation, we eventually reasoned that Belisarius deploys “strategy” contracts, and then calls into them using execute
; the strategy contract is the address being supplied as an argument. execute
then delegatecall
s the strategy contract, and executes using the bytes data as the call it’s supposed to make to the contract (at least as far as we can tell).
If you aren’t familiar with delegatecall
, there are excellent resources out there, but it shouldn’t be too hard to explain it here if you’ve made it this far. delegatecall
allows Belisarius to call the strategy contract and execute code there as if the code was in Belisarius all along. That’s why if you look at anexecute
tx from Belisarius on Etherscan (in regular view) you won’t see it calling another contract to execute the strategy. If you do want to see it, click on a transaction, then click on internal txs, and switch to advanced view. You should be able to see the delegatecall
s to the strategy contract now.
I got a headache trying to figure it out then, but looking at it now, I’m pretty sure that if you follow it all through, the second argument passed to func_0603
is simply the length of the data for the call, and that it slices from after the address argument to the end of the data, and delegatecall
s with that to the address. The temp1
variable is something you’ve probably noticed in Solidity calls - call
and delegatecall
return a boolean indicating if the call was successful or not. That’s the reason it’s checked right after the delegatecall
, and if it isn’t 0x01
- true
- that the whole thing reverts.
wards(address)
Understanding the first execute function helped us understand another function. wards
is the last function in the if
s (jump table). The first check there reverts if there is msg.value
- we can understand that means that it is not a payable function. (Conversely, execute(address,bytes)
did not have this check, meaning that it is payable. I don’t see anywhere in the code of the function that deals with value, leaving me to suspect it was made payable as a gas optimization since doing so avoids needing to check if there is any msg.value
.) Much like we saw at the beginning of the execute function (before jumping to an internal function) it first does some data validation, then hops to an internal function where it checks a mapping using the address supplied as the argument as the key, and returns the value. Easy peasy.
Explained in other words, this is a view function returning a value from the mapping being used as auth - you can check if an address is whitelisted to call functions like execute using this function. In fact, the mapping itself likely has public visibility, and this would be the getter function generated by Solidity for it.
rely
& deny
rely(address)
and deny(address)
were the next ones we figured out. Knowing that there is an access list implies a way to get addresses on (and maybe off) the list, and since both rely
and deny
have address as their lone argument, it stood to reason that it was them. Long story short, it is, you can follow the code yourself, it should be pretty straightforward after the above.
execute(bytes,bytes)
and a big spoiler of the best kindI was still really curious about the second execute
. We had some ideas about what it might be. One of the things that was nagging at me was trying to figure out if there was anything in the contract about deploying strategies, I thought that might happen here. The decompilation was too hard for us to parse, though. I had another thought from looking at all the loops that seemed to be happening was that maybe it was some kind of custom tx bundling like weiroll. We were stumped.
This is where it’s time to reveal a big ‘ol spoiler though. Some time later, I was looking through Maker’s code with a colleague (nothing connected, just independently looking through the repo) when I was confronted by the following code:
mapping(address => uint) public wards;
function rely(address usr) external auth { require(live == 1, “Vat/not-live”); wards[usr] = 1; }
function deny(address usr) external auth { require(live == 1, “Vat/not-live”); wards[usr] = 0; }
While this isn’t the exact same code as Belisarius (for example, Belisarius reverts without any error message), there’s no mistaking that it’s the same functions as Belisarius.
MakerDAO’s contracts were written by DappHub, a group of absolute Solidity chads. The ds you see all over the place in the contracts stands for DappSys, which is what they called their contracts, if I’m not mistaken. If I was more intelligent I would’ve looked to see if I could find execute(bytes,bytes)
somewhere in Maker or DappSys code. I wasn’t that intelligent, and only much later thought of searching .sol files in Github’s advanced search for execute(bytes,bytes
to see if I could find something calling it, which of course ended up sending me to DappSys’s ds-proxy:
// use the proxy to execute calldata _data on contract _code
function execute(bytes memory _code, bytes memory _data)
public
payable
returns (address target, bytes memory response)
{
target = cache.read(_code);
if (target == address(0)) {
// deploy contract & store its address in cache
target = cache.write(_code);
}
response = execute(target, _data);
}
Apparently the original guess had been in the right direction; this execute
function took contract code and calls to make to the contract as arguments, deployed the contract code and then called it with the second argument as a payload.
You can also find the cache
and setCache
functions in ds-proxy, by the way.
All of this points to Belisarius being a DappSys contract, or at least a contract built with DappSys building blocks. To be very clear, I don’t think this even remotely implicates DappHub as being the operators of Belisarius, the same way that they aren’t MakerDAO.
The discoveries with DappSys struck me as potentially useful for another puzzle in Belisarius. As mentioned above, most of the Belisarius’s functions are recognized by 4bytes. There are two that are not: 0x78111f6
and 0xa90e8731
. Perhaps the other DappSys contracts would be a good place to look for a match, though I don’t know an easy way to do this. (A not easy way would be using something like Foundry’s cast, and doing cast sig
on anything promising. Naturally, I've tried this out.)
I suspected originally that this wouldn’t be too hard a task, but has been quite a task, and I’ll defer it to its own chapter.
There are a couple of interesting things I noticed as I was cleaning up a gist attempting to loosely reconstruct Belisarius (linked later) in order to share it. One is yet another DS reference. If you look through the decompilation for the setCache
function, there is a revert there which actually has an error message - most don’t. In the decompilation a couple of lines before, you can see a big hex string get put in memory. Without bothering to check, I suspected that this was the error string.
Looking at ds-proxy, setCache
does indeed have an error string:
“ds-proxy-cache-address-required”
I suspected the big hex was the encoding for the error string, and was curious if it would match up. Try it yourself - the hex string is 0x64732d70726f78792d63616368652d616464726573732d726571756972656400
, and you can search online for a hex to text decoder.
Another point of note was around events. In Etherscan there’s an events tab which displays the events emitted by a contract ordered by most recent first. Glancing at Belisarius’s tab we can see that Belisarius does indeed emit events. topic0
is always the same, which implies that we’re always dealing with the same event. (If you’d like to learn more about events and what topic0
is, I have an old blog post that I wrote for my previous employer, Linum Labs, which I’m personally fond of.)
A control-f search of the decompilation doesn’t turn up any events in the decompiled code (search for log
, which is what the decompiler uses to signify an event being emitted). If you did search, though, you’ll see one does come up in the disassembly. It seems strange for the event to show up in the disassembly without being in the decompilation, and truth be told, it kind of looks like unreachable code in the disassembly. There’s another inconsistency that I caught on to, and there might be even more. Long story short, if you look ahead to the Compiler Version section, you’ll see that this is a red herring, though. This does present a conundrum, albeit one with a potentially simple solution. If Belisarius has no events in the contract, how come Belisarius emits events? A simple answer after decompiling the execute
s would be that the events are actually in the strategy contracts, but emitted from Belisarius since Belisarius is delegatecall
ing in. I don’t know if this is correct, but it’s certainly my current hypothesis.
Looking at ds-proxy (since at least by now I realized I should always be looking at ds contracts to try to place things), you’ll see the note modifier used on execute(bytes,bytes)
and setCache
. note
is imported from the lib folder in ds-proxy (for those of you unfamiliar with DappTools or Foundry, that’s where imported libraries are in those frameworks), and if you follow it through, you’ll find that the note
modifier wraps a function, initializing some variables at the beginning, and emitting an event (LogNote
) using them and some globals at the end. Saying the event is LogNote
doesn’t totally check out, though. If you look at ds-note, the note event is anonymous with four indexed arguments. This should generate 4 topics. Belisarius, however, uses LOG1
, which means only one topic is being emitted. Also, we can see that the event is also emitted when execute(address,bytes)
is called, not just execute(bytes,bytes)
, but at first glance not every time that execute
is called, which implies that the operators have a way to elect to emit the function or not. This implies a few mysteries which I’ll need to save for later (or never).
Going back to the actual event, if you put topic0
into samczsun’s signature database (which allows you to search entire signatures and also text-to-selector/signature, unlike 4bytes, which is only selector-to-text) we can actually determine exactly what the event is:
log_named_uint(bytes32,uint256)
I had a suspicion this would be in DappSys’s ds-test, which it is. One noteworthy difference is that the current ds-test has a slightly different signature, using string
for the first argument instead of bytes32
like Belisarius does. If you look at the version of ds-test from back when Belisarius was deployed, though (here), you’ll see ds-test was indeed using bytes32
as the first argument then.
bytes32
is often a cheaper way to work with strings than string, and DappSys in particular seemed to embrace that, from what I remember. (One of their devs told me once about something completely different that it’s always better to use a fixed-size datatype than a dynamic one, another possible reason for the preference.) Playing around with Belisarius’s events on Etherscan, it does indeed seem that the first field in the event is being used for text, all of the ones I looked at said “availableAmount”, which in turn makes it stand to reason that the second argument represents some value in wei or something like that. If I wasn’t lazy I’d match it up against the msg.value
and/or remaining value to Belisarius after the transaction. I am lazy, though. Back when I thought that this was an event on Belisarius itself, I took some placeholder shots in the dark on the gist, which at this point I don’t even think could be right. Basically I put that the bytes32
is parsed from calldata in the same place where LogNote
’s bar argument was. I put msg.value
as the second arg since I’m lazy and it’s easier than trying to code something to figure out how much value is left after the delegatecall
. I also put the note
modifier on both execute
s (unlike ds-proxy), and removed it from setCache
since I have a suspicion that it’s only for the value-moving txs. (I also modified the note
modifier to fire the log_named_uint
event instead of LogNote
.) As stated before, it doesn’t seem to fire on every execute
, so there’s obviously a lot missing here, but I don’t know what the guidelines are, so I left that out too.
The last thing I’d like to point to in this section are return values. In the gist where I take a shot at reconstructing Belisarius I keep the return values from the ds-proxy functions they’re based on and maybe copied from, but I am not too sure from the compilation that Belisarius’s implementation actually returns anything.
Another interesting detail, though perhaps of no import in unraveling Belisarius’s components, is what compiler version was used. Someone who’s way better than me at this told me that the Solidity used for the contract is pre-Constantinople. The Constantinople hardfork happened at 7,280,000 so it was technically live when Belisarius was deployed, but the compiler used to compile the bytecode does not use opcodes included in Constantinople, meaning the version of the compiler being used hadn’t implemented them yet. The opcodes pointed out to me were native bitshift opcodes like SHR (from EIP-145).
Looking through Belisarius’s disassembly, though, you can see that there is actually on SHL (left bitshift) towards the very end, right around where the only LOG
opcode is. This is very strange, since if the Constantinople opcodes were available, the optimizer should have prioritized them over division for bit operations, which is not the case (as mentioned above). Someone who saw an earlier version of this shed some light on both the SHL and the LOG. Solidity has a spec for contract metadata, which is encoded at the end of the contract. At least in this contract, it would seem the last 43 bytes are reserved for the Swarm hash of the metadata. This means that both the SHL
and LOG1
, despite being the right hex codes for those opcodes, are actually just part of a non-EVM encoding, and can be totally ignored.
This was part of a larger conversation about the compiler version, itself. The same person who tipped me off to the metadata has resources about deducing the Solidity version of a contract by analyzing the initial bytecode. Most Solidity starts with 60806040 (which is how it initialized the free memory pointer), but the continuation of the string often helps narrow down to a more specific subset of versions. From Belisarius’s opening 608060405260043610
, he was able to narrow it down to 0.4.22 through 0.5.1. Once the field was that small, I did some more digging by sifting through the changelogs for those versions of Solidity to see if there was something that would help me narrow it down even further. As it happens, as far as I can tell staticcall
was only introduced in 0.5.0, and is used by Belisarius, which should mean that Belisarius was compiled with either 0.5.0 or 0.5.1.
Also, as far as I can tell there should be some way to extract the version from the Swarm hash, but I do not know how. I assume this is from when Swarm was a bit of a network than it is now, and any Swarm gateway I’ve tried with the metadata hash come up empty, which leads me to believe that the data is no longer on their network. There still might be someone who maintains a mapping of particular hashes to versions, if anyone knows anything about this, please let me know.
I’ve been keeping a bit of a reconstruction of Belisarius in a gist. It isn’t meant to produce the same exact bytecode as Belisarius, and indeed won’t even compile as of this writing since I just use the selector to name the functions I don’t know the name of. You can find it here.
Just to be very clear, much of what is there is guesswork. Like I mentioned before, it isn’t meant to be a 100% recreation, more of an idea of what’s going on in Belisarius.
(Though it has spoilers for 0x78e111f6
, so don’t ruin the fun for yourself before you read the next chapter.)
This should give some initial insight into Belisarius. Truth is, this doesn’t really have any bearing on understanding the actual txs Belisarius performs, most likely, though for me this isn’t really about cutting straight to the competitive part. It’s fun for me to just noodle around the bytecode and see what we can get out of it. If it’s also fun for you, maybe we can find a support group and go together.
There’s so much more to do here! There are still two functions we haven’t cracked, I plan on dedicating the next chapter to getting what we can figure out about them written down. That’s all before we start really analyzing the ops, who’s calling Belisarius, what they’re doing, how they’re doing it, and whatever else we can learn by checking the chain.
I’m hoping this was interesting, useful, and/or informative for you, anon. Like I mentioned before, much of this is built on foundations much larger than myself, and making frens along the way has been very enriching. If you’d like to collaborate, feel free to reach out.