Test Driving an Ethereum Solidity Contract — Iteration 3: ERC20 Token

Christoph
7 min readApr 2, 2018

Previously on Test Driving an Ethereum Solidity Contract:

We’re into the home stretch! Here’s what we have left to build in the interface:

Feature: Allowance

As a Blockchain User I want to be able to see how many tokens a Token holder has allowed someone else to spend on their behalf.

Passing in owner and spender addresses returns amount

This one’s another Catch-22. I can’t validate allowance() if approve() doesn’t exist to let me approve the transferring tokens. And approve() can’t return a spender’s allowance if I don’t have approve() to set it. As before, we’ll create a basic approve() and then backfill the tests as needed.

Getting our first allowance test to pass is easy:

However, as is usually the case, a second test having to support a different account with a different amount quickly breaks such trickery.

Now we’re going to have to get a little fancier. We need a data structure that allows us to store the accounts and amounts for every account that allows token transfer on their behalf. For that, Solidity offers us the mapping of a mapping. We’ll add one right below our balances mapping:

mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowed;

And here’s how we get our tests to pass:

GIT: 631e561f0dd8ee1fa4d6b7b2edfd4aa582f1fe33

Returns zero if none have been approved

Because the Solidity docs indicate that a mapping’s default value is all zeros, I’m hoping that this req works out of the box, without me having to update the contract.

Let’s see if it does:

Sure enough, we’re green. Just to be sure, temporarily change the test value to 1 and make sure that the test fails. It’s real easy to just assume that because your test passes that your code is working, but unless you’ve seen the progression from red to green, it could just be returning you a false positive.

GIT: b14cec3c833802862df0a91c377acd61cb6bbf4d

Feature: Approve

As a token holder I want to allow another address to transfer tokens on my behalf.

Passing in address of spender and amount returns true if possible

A few things. I’m pretty happy with the tests I have in terms of validating that approve() works. I could add some updated descriptions to my tests, but for now I’m going to declare victory.

Also, I have not been able to find a way to validate a return value on a transactional function call — as in one that changes the state of the contract. Everything I’m finding tells me you can’t. If a kind reader knows of a way, please add a comment.

Finally, I’m wondering if there are any negative boundaries that we should test for? What if the holder doesn’t have the tokens for which she’s approving transfer?

I’m thinking that this one is a matter of preference. We have yet to do transferFrom(). For that we’re going to have to ensure that the tokens are available for transfer there. Rather than use up gas twice — once for approve, and once for transferFrom — I’m going to let them approve as much as they want. If they don’t have it, an error will be thrown when the approved party tries to transfer funds that aren’t there. Granted, this means less work for me, but at least I’m being honest. This is one of those moments when you’d want to have a conversation with your stakeholders and let them know the options. This is not something that they’d want to learn about after it’s pushed out to the main blockchain.

No changes.

Emits an Approval event when successful

We’ve been down this road before.

To make it green, we add this Approval event next to the Transfer event, and update approve() so that it emits the event.

GIT: 926acae31145ff130600c8730014a2396f44d228

Feature: Transfer From

As a third party I want to be able to transfer Token from one address to another when I've been approved.

Approved third party address is able to transfer tokens from an approved address to another address

OK, while things are getting progressively more complicated, this is still pretty straightforward. Approve someone else to transfer funds on our behalf. Have them transfer the funds to another account. Get the balance of that account, and make sure that the funds we authorized are there. Here’s our test:

And here’s what it takes to make it green:

Ship it!

GIT: e08c5e6db140a4734acd8dfd54aa5249c2d6781b

Wait… not so fast. Can you figure out what’s wrong?

Our requirement are to allow the transfer funders by an “approved” third party. This code let’s anybody do it. This code is doing a lot, so we need to make sure that we test for all of our negative boundary conditions. Let’s try to think of them all:

  1. The account where the funds are being transferred from should have sufficient funds.
  2. Only the approved party should be allowed to transfer funds.
  3. The approved party should not be allowed to transfer more than the amount authorized.
  4. The authorized accounts allowance amount should go down after transferFrom is called.

I’m thinking #1 and #3 are the same test, with the others being their own test.

First off, let’s create a test where we have an account with 99 tokens, and we then try to have it transfer 100:

Since this is a negative test, my final version will trap the exception. However, to start off with, I’ve coded my test without trapping the exception so that I can see the expected behavior that I an looking for. Sure enough, when I run it, it throws an exception.

For this test, I’ve added a new account, accountWith99, to use for this test. Just to be on the safe side, I’ve added a test to insure that its balance is 99 tokens.

Now let’s trap the exception so that our test is green. We’ll be doing the same pattern we did before.

The cool thing is that our contract code didn’t have to change at all in order to get the test to pass. That doesn’t mean that it was a waste of time. Don’t just trust that your code does what you think it should, verify it.

Доверяй, но проверяй Doveryai, no proveryai

(Trust but verify)

GIT: f6c9e05f65136089fee3435eb1c9c16d85d48ed0

Now, let’s try to have someone transfer funds who isn’t authorized to do so and see what happens.

Going over the test: first off, I make sure that I have an account that isn’t allowed to transfer anything from the owner account. Then, I make sure that the account I’m going to try to transfer tokens to has a zero balance. Finally, I have the nilAddress account attempt to transfer funds to the holder account from the owner account.

Rutroh. Right now it looks like anybody can transfer funds on another account’s behalf. That’s not good. Let’s add an assertion that our holder account balance is still zero.

So, even though the transferring account wasn’t authorized to do so, it still was able to transfer tokens to the holder account.

Adding one line to the contract gives us the error we want.

Try running the test now. You should see a VM exception.

Let’s update the test to catch the exception, and we should be good.

GIT: 0ccecc9f625492df83aaa84c9269e69d49805903

There’s one final thing that we need to test for. Right now, I’m thinking that an authorized account can make the transferFrom call as many times as it likes because their allowance isn’t going down. Let’s add a test that checks for that.

We authorize holder to transfer 15 tokens. We then have holder transfer 7. Their allowance should be reduced to 8.

Sure enough, our allowance isn’t going down after we call transferFrom(). Let’s fix that by subtracting from their allowance.

And now we’re green.

GIT: 23737f5be71c252f084041cf31cea65d61b1e7ce

Emits a Transfer Event

Ahh, the good ol’ event test…

And we add the Transfer event call to make it green…

GIT: 343178aef9949b316cfa21c4027869de16779afa

And there you have it, a test driven ERC20 contract ready to deploy onto the blockchain — and that’s exactly what we’re going to do next time.

In our final installment of this series, we’ll migrate our contract to each of the Ethereum test nets and go over the differences among them. We’ll also fire up a truffle console and show you how you can use it to interact with your contract from there.

See you then.

--

--