From zero to nowhere: smart contract programming in Huff (4/4)

Laurence van Someren
The Aztec Labs Blog
9 min readAug 30, 2019

--

You might have been wondering something while reading the last Huff ERC20 blog post: why on earth would you want to implement an ERC20 smart contract in Huff? It’s a fair question, with no good answer. But this series is like an oversized bowl of cornflakes: we have to finish it, even if we get bored of it. So let’s soldier on.

Thankfully, we only have one thing left to implement — or rather, three things which together form one big feature. It’s as if God is part of the ERC20 specification.

Allowances, approval, and ‘transferring from’

Currently, we can check the token balance held by any address, transfer tokens to any address, and see the total number of tokens in existence. But suppose you want to use these tokens to buy something from someone. You can transfer the necessary tokens to their account, but how will they know when you’ve done so? They’ll have to keep checking the event logs looking for a transfer from your address to theirs, which isn’t particularly practical, and can’t be done from within a contract. Wouldn’t it be better if you could approve some tokens to their account (without transferring them) so that they could then transfer them from your account in their own time? That way, they could be sure they’d received them — and better still, they’d be able to do this from within another smart contract. This is the premise of the remaining three functions: allowance, approve, and transferFrom.

The ERC20 interface uses a private nested mapping allowances[owner][spender], representing the number of tokens spender is allowed to take from owner. This can be read with the allowance(address owner, address spender) method, and is set with approve(address spender, uint value). Finally, transferFrom(address from, address to, uint value) takes value tokens from the address from, approved for the msg.sender by the address from, and transfers them to the address to, reducing allowances[from][msg.sender] by value.

As an example, suppose the address 0xa1fa1fa wants to let the address 0xcabba9e take 100 tokens, so that 0xcabba9e can send them to 0xbeef. This is what happens:

  1. 0xa1fa1fa calls approve(0xcabba9e, 100). This sets allowances[0xa1fa1fa][0xcabba9e] to 100, but leaves all balances unchanged.
  2. allowance(0xa1fa1fa, 0xcabba9e) returns 100.
  3. 0xcabba9e calls transferFrom(0xa1fa1fa, 0xbeef, 100). This takes 100 tokens from 0xa1fa1fa’s account and sends them to 0xbeef.
  4. allowance(0xa1fa1fa, 0xcabba9e) returns 0. balanceOf(0xbeef) returns 100.

Here are the three functions in Solidity:

function approve(address spender, uint value) public returns (bool success) {
allowances[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}

function transferFrom(address from, address to, uint value) public returns (bool success) {
balances[from] = balances[from].sub(value);
allowances[from][msg.sender] = allowances[from][msg.sender].sub(value);
balances[to] = balances[to].add(value);
emit Transfer(from, to, value);
return true;
}

function allowance(address owner, address spender) public view returns (uint remaining) {
return allowances[owner][spender];
}

Reading allowed

allowances[owner][spender] is a two-dimensional mapping. How does that work? Let’s think about balances[owner] first.

To get balances[owner], we take the hash of owner and BALANCE_LOCATION (which we’ve defined to be 0x00) together, which gives us the storage key for the relevant balance.

To get the key for allowances[owner][spender], then, it makes sense that we should take the hash of owner with ALLOWANCE_LOCATION (whatever we decide that should be), and take the hash of spender with the result of the first hash to get the appropriate storage key.

Since BALANCE_LOCATION is 0x00, OWNER_LOCATION is 0x01, and TOTAL_SUPPLY_LOCATION is 0x02, ALLOWANCE_LOCATION will be equal to 0x03.

#define macro ALLOWANCE_LOCATION = takes(0) returns(1) {
0x03
}

We’re now in a position to implement allowance(address owner, address spender). This is much like balanceOf(address owner), only with a two-dimensional mapping:

template <error_location>
#define macro ERC20__ALLOWANCE = takes(0) returns(0) {
UTILS__NOT_PAYABLE<error_location>()
// Load _owner onto the stack and store at 0x00
0x04 calldataload ADDRESS_MASK()
0x00 mstore
// Store ALLOWANCE_LOCATION at 0x20 and hash these two values
ALLOWANCE_LOCATION() 0x20 mstore
0x40 0x00 sha3
// stack: key(allowances[owner])
// Store this at 0x20 and store spender at 0x00
0x20 mstore
0x24 calldataload ADDRESS_MASK()
0x00 mstore
// Hash key(allowances[owner]) and spender together to get key(allowances[owner][spender])
0x40 0x00 sha3
// sload to get allowances[owner][spender]
sload
// Store at 0x00 and return
0x00 mstore
0x20 0x00 return
}

Now that we have an allowances[owner][spender] getter function, it makes sense to write a setter. This will have the added excitement that we’ll need to emit an Approval event.

approve(address spender, uint value)

approve might be the most important of all the ERC20 methods, because it provides the two things we all spend our lives seeking: money and approval.

First, we want to load our Approval event topics onto the stack. Here’s how the event is defined:

event Approval(address indexed owner, address indexed spender, uint value)

Like in the case of the Transfer event, we want to store value at 0x00 in memory, since it’s not indexed, and call log3 with the following stack state (as usual, with the top item on the left). But don’t forget to check that callvalue is zero first!

UTILS__NOT_PAYABLE<error_location>()
0x00 0x20 signature owner spender

As you can see, like the Transfer event, the Approval event has its own signature:

#define macro APPROVAL_EVENT_SIGNATURE = takes(0) returns(1) {
0x8C5BE1E5EBEC7D5BD14F71427D1E84F3DD0314C0F7B2291E5B200AC8C7C3B925
}

(This is just the Keccak-256 hash of Approval(address,address,uint256).)

Let’s get these parameters onto the stack:

0x04 calldataload ADDRESS_MASK() // spender
caller
APPROVAL_EVENT_SIGNATURE()
0x20
0x00

And load value with calldataload, duplicating it as we’ll need to store it at key(allowances[msg.sender][spender]) and at 0x00 in memory for the Approval event:

0x24 calldataload
dup1

At this stage, we need to calculate key(allowances[msg.sender][spender]), and store value there. This is not much different to allowance(...):

caller 0x00 mstore
ALLOWANCE_LOCATION() 0x20 mstore
0x40 0x00 sha3
// stack: key(allowances[msg.sender]) value value 0x00 0x20 signature msg.sender spender
0x20 mstore
dup7 0x00 mstore
0x40 0x00 sha3
// stack: key(allowances[msg.sender][spender]) value value 0x00 0x20 signature msg.sender spender
sstore

This leaves the stack like so:

value 0x00 0x20 signature owner spender

All that’s left to do is to store value at 0x00 in memory, emit the event with log3, and return true:

0x00 mstore
// stack: 0x00 0x20 signature owner spender
log3
// stack is empty
0x01 0x00 mstore
0x20 0x00 return

And just like that, msg.sender has approved value tokens for spender. Now spender just needs a way to spend them. Well, they’re in luck, because I was just about to implement

transferFrom(address from, address to, uint value)

Let’s recap what transferFrom(address from, address to, uint value) is supposed to do:

  1. Take value tokens from from and transfer them to to
  2. Reduce allowances[from][msg.sender] by value

To be more precise, we need to do the following:

  1. Load Transfer event topics onto the stack
  2. Increment balances[to] by value
  3. Decrement balances[from] by value
  4. Decrement allowances[from][msg.sender] by value
  5. Error-handling
  6. Emit the Transfer event
  7. Return true

Sound familiar? These steps are more or less the same as those of transfer(...) from part 2. The difference is transfer takes tokens from msg.sender, whereas transferFrom takes them from from, which needn’t be the same address. transfer doesn’t have step 4, either. But otherwise, they’re pretty similar, and we’ll be able to reuse a lot of code.

Now look at the equivalent of step 1 for transfer(...):

#define macro ERC20__TRANSFER_INIT = takes(0) returns(6) {
0x04 calldataload ADDRESS_MASK()
caller
TRANSFER_EVENT_SIGNATURE()
0x20
0x00
0x24 calldataload
// stack: value 0x00 0x20 signature from to
}

This can be reused with a few modifications. Firstly, since to is the second argument of transferFrom(address from, address to, uint value), we need to replace the 0x04 in the first line with 0x24. Secondly, we’re transferring from from, not message.sender (although the two can be the same), so caller needs to be replaced with 0x04 calldataload ADDRESS_MASK(). Finally, since value is the third argument of transferFrom, not the second, the final 0x24 needs to be 0x44. So here’s our ERC20__TRANSFER_FROM_INIT macro:

#define macro ERC20__TRANSFER_FROM_INIT = takes(0) returns(6) {
0x24 calldataload ADDRESS_MASK()
0x04 calldataload ADDRESS_MASK()
TRANSFER_EVENT_SIGNATURE()
0x20
0x00
0x44 calldataload
// stack: value 0x00 0x20 signature from to
}

For step 2, the existing ERC20__TRANSFER_GIVE_TO macro can be reused in its entirety:

#define macro ERC20__TRANSFER_GIVE_TO = takes(6) returns(7) {
// stack: value 0x00 0x20 signature from to
dup6 0x00 mstore
0x40 0x00 sha3
// stack: key(balances[to]) value 0x00 0x20 signature from to
dup1 sload // balances[to] key value 0x00 0x20 signature from to
dup3 // v b k v 0x00 0x20 sig f t
add // v+b k v 0x00 0x20 sig f t
dup1 // v+b v+b k v 0x00 0x20 sig f t
dup4 // v v+b v+b k v 0x00 0x20 sig f t
gt // v>v+b v+b k v 0x00 0x20 sig f t
swap2 // k v+b v>v+b v 0x00 0x20 sig f t
sstore // v>v+b v 0x00 0x20 sig f t
}

This adds value to balances[to] and leaves an error code on the stack.

Step 3 is slightly more complicated. ERC20__TRANSFER_TAKE_FROM doesn’t quite do what we want, since it always takes tokens from msg.sender rather than from:

#define macro ERC20__TRANSFER_TAKE_FROM = takes(7) returns(8) {
// error_code _value 0x00 0x20 signature from to
caller 0x00 mstore
0x40 0x00 sha3
// key(balances[from]) error_code value 0x00 0x20 signature from to
dup1 sload // balances[from] key error value 0x00 0x20 sig f t
// b k e1 v 0 2 s f t
dup4 dup2 // b v b k e1 v 0 2 s f t
sub dup5 // v (b-v) b k e1 v 0 2 s f t
swap3 // k (b-v) b v e1 v 0 2 s f t
sstore // b v e1 v 0 2 s f t
lt // error2 error1 value 0x00 0x20 signature from to
}

We could just copy this macro, replacing caller with dup6. But surely, I hear you cry, there’s a better way which avoids us having two macros which do almost exactly the same thing? You’re right, there is. This is where Huff’s template parameters come in handy.

Instead of having caller hard-coded into this macro, we can replace the caller opcode with a template parameter <from>, and use the same macro for both transfer and transferFrom. Remember that we’ll need to add this line before our ERC20__TRANSFER_TAKE_FROM macro definition:

template <from>

Then, when transfer uses the ERC20__TRANSFER_TAKE_FROM macro, it will call it like this:

ERC20__TRANSFER_TAKE_FROM<caller>()

while transferFrom will do this:

ERC20__TRANSFER_TAKE_FROM<dup6>()

Simple.

It’s worth noting that we could have used dup6 rather than caller in the first place — but caller has a gas cost of 2, compared to 3 for dup* — and that one gas could make all the difference, right? Waste not, want not.

Step 4 is much like ERC20__TRANSFER_TAKE_FROM, but with allowances rather than balances. Let’s call our macro ERC20__TRANSFER_SUB_ALLOWANCE:

#define macro ERC20__TRANSFER_SUB_ALLOWANCE = takes(8) returns (9) {
// stack: error2 error1 value 0x00 0x20 signature from to
dup7 0x00 mstore
ALLOWANCE_LOCATION() 0x20 mstore
0x40 0x00 sha3
// stack: key(allowances[from]) error2 error1 value 0x00 0x20 signature from to
0x20 mstore
caller 0x00 mstore
0x40 0x00 sha3
// stack: key(allowance[from][msg.sender]) error2 error1 value 0x00 0x20 signature from to
dup1 sload // allowance key e2 e1 v 0x00 0x20 s f t
dup5 dup2 // a v a k e2 e1 v 0 2 s f t
sub dup6 // v a-v a k e2 e1 v 0 2 s f t
swap3 sstore // a v e2 e1 v 0 2 s f t
lt // error3 error2 error1 value 0x00 0x20 signature from to
}

Now handle all these potential errors:

callvalue or or or <error_location> jumpi

Finally, emit the event and return true:

0x00 mstore // store value at 0x00 first
log3
0x01 0x00 mstore
0x20 0x00 return

And we have a complete transferFrom function!

template <error_location>
#define macro ERC20__TRANSFER_FROM = takes(0) returns(0) {
ERC20__TRANSFER_FROM_INIT()
ERC20__TRANSFER_GIVE_TO()
ERC20__TRANSFER_TAKE_FROM<dup6>()
ERC20__TRANSFER_SUB_ALLOWANCE()
// error3 error2 error1 value 0x00 0x20 signature from to
or or <error_location> jumpi
// value 0x00 0x20 signature from to
0x00 mstore log3
0x01 0x00 mstore
0x20 0x00 return
}

And there we have it: a highly-optimised ERC20 implementation in Huff. You can view the full source code, and an accompanying JavaScript interface, in our GitHub repository.

How efficient is this Huff contract?

Our ERC20 contract is definitely more gas-efficient than one written in Solidity (see below) — but only marginally so. transferFrom, the method with the largest gas saving, costs around 1,000 less gas in our implementation. This may sound like a lot, but it’s nothing compared to the cost of storing data in storage (either 5,000 or 20,000 gas) and the 21,000 gas base cost of each transaction.

This graph leaves out the 21,000 gas base cost of every transaction, and the cost of storing data in the contract storage with ‘sstore’, which varies depending on whether the relevant storage slot has been set before, and what value it’s being set to.

Of course, writing slightly more efficient ERC20 contracts isn’t the main purpose of Huff. So far, it has been used much more productively in Weierstrudel and Oliver — contracts for multiplying elliptic curve points in Weierstrass and Twisted Edwards curves, respectively. But it’s nice to know that even the simple things are more efficient in Huff.

--

--