Exploring an ERC20 Token Contract

KC Tam
10 min readDec 17, 2018

--

Overview

In this article, we are exploring a live ERC20 Token Contract from Paxos (PAX). As PAX is a stablecoin and subject to regulation, we can see the additional functions besides those required in ERC20. We will deploy the PAX token contract onto our Ethereum simulator, make observation on how the contract is constructed and simulate some operations in this contract.

VERY IMPORTANT NOTE: Here we use Paxos Standard tokens just for sake of studying token contract. The article is purely for technical illustration on the contract itself and how things work. The author is by no means promoting any tokens or providing any investment advices on any specific tokens.

Prerequisite

This article assumes reader having certain understanding about Ethereum network, its smart contract capability, and some Solidity basic knowledge. We do not go into detail on Solidity, but will refer to some lines of code from the original contract.

Overview of ERC20 Token

ERC20 is the standard of tokens issued on Ethereum platform. There are more than 150,000 ERC20 standard tokens nowadays in the token market, according to Etherscan information (Dec. 16, 2018). The ERC20 token is implemented as a contract on Ethereum, which is most likely written in Solidity. After the contract is compiled and deployed in the Ethereum mainnet, it becomes the real tokens, which can be owned by Ethereum accounts and made transferrable between accounts.

ERC20 Token Standard is defined in EIP20 (link). The motivation of EIP20 is to provide a standard interface such that applications like wallets or other DApps (decentralized applications) can interact with the token contract, and hence “use” the tokens.

Note that ERC20 does not come with the actual implementations of functions. It is the token contract owner (or issuer) to do the actual coding.

There are some ready-to-use implementations in the community. Among them, OpenZeppelin is a popular one (link). By providing some basic information (e.g. token name and symbol) and the nature (of fixed supply or mintable, etc), one can easily launch one’s own token in Ethereum network. Alternatively token owners can code by themselves for whatever reasons. As far as they implement the functions specified in EIP20 with proper logic, the tokens are of ERC20 “standard”. In PAX token contract we are illustrating, they have coded their own token contract, and we can take a glimpse on their implementation.

In real life, many tokens require additional capability. And it is the token owner determining what additional functions are needed. For example, being a stablecoin, Paxos involves the token minting and burning when needed. It also opens up law enforcement as it is subject to regulation. That is why we see additional functions in the PAX token contract. Nevertheless, as far as tokens like Paxos have implemented the required functions in EIP20, they are still ERC20 standard tokens.

Setup

We are deploying the PAX token contract in Ganache-CLI Ethereum simulator. This simulator comes with 10 accounts with 100 ethers on each of them. We will use the Truffle framework for contract deployment (migration), and geth client for contract interaction.

There are several roles in PAX token contract.

For better illustration on how the token contract is working, we are defining the roles as following, based on the PAX contract code:

Roles for various built-in accounts

Prepare the Environment

The preparation is composed of the following steps.

  1. Download the tools
  2. Prepare the three terminals
  3. Download the project (PAX Contract)
  4. Run Ethereum Simulator (Ganache-CLI)
  5. Modify truffle.js file
  6. Compile the contract code
  7. Deploy (migrate) contract to Ganache-CLI
  8. Access contract using geth console
  9. Check contract readiness for interaction

1. Download the Tools

We are using the following tools for this illustration.

2. Prepare the Three Terminals

3. Download the Project (PAX Contract)

In Truffle Terminal

$ git clone https://github.com/paxosglobal/pax-contracts.git
$ cd pax-contracts
$ ls
Truffle Terminal

4. Run Ethereum Simulator (Ganache-CLI)

In Ganache-CLI Terminal

$ ganache-cli <-m nmenonic>

In this terminal I use the mnemonic. It is just for keeping the same set of addresses. If you do not provide any mnemonic, a new set of 10 addresses will be generated each time.

Ganache-CLI Terminal

5. Modify truffle.js File

We modify the truffle.js to take out those we don’t need in our illustration. The truffle.js we are using is like this.

module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
networks: {
development: {
host: '127.0.0.1',
port: 8545, // ganache-cli
network_id: '*', // Match any network id
gas: 6700000,
gasPrice: 0x01
}
},
solc: {
optimizer: {
enabled: true,
runs: 200
}
}
};

6. Compile the Contract Code

We are ready to compile the code. In Truffle Terminal

$ truffle compile
Truffle Terminal

We can ignore the warning messages. Now the artifacts are stored in ./build/contracts

7. Deploy (Migrate) Contract to Ganache-CLI

Next we migrate the contract to our Ethereum simulator. On Truffle Terminal,

$ truffle migrate
Truffle Terminal

And from the Ganache-CLI Terminal, we see some transactions during contract migration. From these transactions we can locate the Contract Address of the deployed contract.

Ganache-CLI Terminal

So in our case, the Contract Address is “0x6af8c2972a3e6858c30d2b631b90080fbb6798bc”. We will use it when interact with this deployed contract.

8. Access Contract using Geth Console

Now we are on the Geth Console. We access Ganache CLI using RPC.

$ geth attach http://localhost:8545
Geth Console

To access the contract we define the ABI (application binary interface, see Appendix) and use the command eth.contract().

> var abi = <abi shown below>
> var tokenContract = eth.contract(abi).at(<contract_address>)
Define ABI (truncated)
Define Object for Deployed Contract

9. Check Contract Readiness for Interaction

To check if the contract is accessed, we can check if anything in the contract is accessible. Here we are accessing the public state variable owner defined in the contract. According to the contract the owner is the account address who deploys the contract. Since Truffle uses eth.accounts[0] to deploy the contract, we will see the account is the Account[0].

> tokenContract.owner()> eth.accounts[0]
After deployment, variable owner is now eth.accounts[0]

So the tokenContract is the object of our deployed contract, and we can execute functions defined in our contract code using tokenContract.

Illustration of Token Operation

We will interact with the deployed contract using tokenContract in Geth console. If some functions are to update the ledger (state), we will see transaction is shown on the Ganache-CLI.

The process of our illustration includes the following steps,

  1. Basic information and operation before PAX circulation
  2. Purchase new tokens from PAX supplier
  3. Use of tokens: transfer between users
  4. Law enforcement: freeze account
  5. Law enforcement: wipe out account
  6. Redemption

Step 1: Basic Information and Operation Before PAX Circulation

Step 1.1: Check the basic information about the token: name, symbol, decimals, total supply of tokens initially

> tokenContract.name()
> tokenContract.symbol()
> tokenContract.decimals()
> tokenContract.totalSupply()

This aligns to the contract code (see lines 38–40 and 114 in the contract in Appendix)

Step 1.2: Check Owner, Supply Controller and Law Enforcer

> tokenContract.owner()
> tokenContract.supplyController()
> tokenContract.lawEnforcementRole()

Per contract design, the Owner and Supply Controller is the one who deploys this contract (i.e. eth.accounts[0]). And no initial Law Enforcer initially. See line 112, 113 and 115 in the contract.

Step 1.3: Assign the Role of Supply Controller and Law Enforcer. Check the roles again.

> tokenContract.setSupplyController(eth.accounts[8], {from:eth.accounts[0]})
> tokenContract.supplyController()
> eth.accounts[8]
> tokenContract.setLawEnforcementRole(eth.accounts[9], {from:eth.accounts[0]})
> tokenContract.lawEnforcementRole()
> eth.accounts[9]

Note that a transaction hash (transaction ID) is sent back after the two “set” functions. And you should see the transaction shown in Ganache-CLI as well.

The roles are rightly set.

Step 2: Purchase New Tokens from PAX Supplier

Assumes User #1 and User #2 has purchased 20,000 and 10,000 PAX directly from Paxos with Fiat currency. After all legal processes required complete, Supply Controller will generate 30,000 PAX, and transfers them to User #1 and User #2 according to the amount they purchased.

Step 2.1: Begin the PAX circulation by unpause().

> tokenContract.paused()
> tokenContract.unpause({from:eth.accounts[0]})
> tokenContract.paused()

Note that only owner can toggle the pausing status. If we try another account (say eth.accounts[1]) the “unpause” is unsuccessful.

Step 2.2: Increase supply by 30,000 PAX. Check the balance of Supply Controller and the Total Supply. All tokens are in Supply Controller at this moment.

> tokenContract.balances(eth.accounts[8])
> tokenContract.totalSupply()
> tokenContract.increaseSupply(30000, {from:eth.accounts[8]})> tokenContract.balances(eth.accounts[8])
> tokenContract.totalSupply()

This process is done by increaseSupply(). You can see how it is implemented in contract code (line 359–360).

Step 2.3: Transfer 20,000 PAX to User #1 and 10,000 PAX to User #2. Check balance of User #1, #2 and Supply Controller. Also check the Total Supply.

> tokenContract.balanceOf(eth.accounts[1])
> tokenContract.balanceOf(eth.accounts[2])
> tokenContract.transfer(eth.accounts[1], 20000, {from:eth.accounts[8]})
> tokenContract.transfer(eth.accounts[2], 10000, {from:eth.accounts[8]})
> tokenContract.balanceOf(eth.accounts[1])
> tokenContract.balanceOf(eth.accounts[2])
> tokenContract.balanceOf(eth.accounts[8])
> tokenContract.totalSupply()

The function transfer() is an ERC20 function. You can see how it is implemented in the contract code (Line 149–150). It does not impact the Total Supply.

Step 3: Use of Tokens: Transfer between Users

Assumes User #1 makes a transaction to User #3, paying User #3 4,000 PAX.

Step 3.1: Transfer 4,000 PAX from User #1 to User #3. Check the balance of User #1 and #3, and Total Supply.

> tokenContract.balanceOf(eth.accounts[1])
> tokenContract.balanceOf(eth.accounts[3])
> tokenContract.transfer(eth.accounts[3], 4000, {from:eth.accounts[1]})> tokenContract.balanceOf(eth.accounts[1])
> tokenContract.balanceOf(eth.accounts[3])
> tokenContract.totalSupply()

Step 4: Law Enforcement: Freeze Account

Assumes User #3 is under legal process and an account freezing is requested by the court.

Step 4.1: Law Enforcer checks the current freezing status of User #3. By default it is not frozen. Now Law Enforcer freezes User #3. Check the freezing status again on User #3.

> tokenContract.isFrozen(eth.accounts[3])> tokenContract.freeze(eth.accounts[3], {from:eth.accounts[9]})> tokenContract.isFrozen(eth.accounts[3])

Again, we try to enforce the freezing process by anyone other than Law Enforcer (even the token owner), and we expect failure.

Step 4.2: If User #1 now wishes to transfer 1,000 PAX to User #3, it will fail as User #3 account is frozen.

> tokenContract.transfer(eth.accounts[3], 1000, {from:eth.accounts[1]})

The checking of account freezing is done on transfer(). See line 146 in the contract.

Step 5: Law Enforcement: Wipe Out Account

Assume court decides to confiscate all the tokens of User #3. Order is issued to Law Enforcer for this legal action.

Step 5.1: Law Enforcer wipes the User #3 account. After that, we check the balance of User #3 and the Total Supply.

> tokenContract.balanceOf(eth.accounts[3])
> tokenContract.totalSupply()
> tokenContract.wipeFrozenAccount(eth.accounts[3], {from:eth.accounts[9]})> tokenContract.balanceOf(eth.accounts[3])
> tokenContract.totalSupply()

Again, no one (not even the token owner) but the Law Enforcer can do so. We will see after wiping, the account balance becomes zero. And the Total Supply also reflects this wiping.

The wiping process is done on function wipeFrozenAddress(). See how it is implemented in the contract code (line 319–320).

Step 6: Redemption

Assume User #2 decides to redeem 8,000 PAX (get back the Fiat currency).

Step 6.1: User #2 transfers 8,000 PAX to Supply Controller. Check the balance of User #2 and Supply Controller, and the Total Supply.

> tokenContract.balanceOf(eth.accounts[2])
> tokenContract.balanceOf(eth.accounts[8])
> tokenContract.transfer(eth.accounts[8], 8000, {from:eth.accounts[2]})> tokenContract.balanceOf(eth.accounts[2])
> tokenContract.balanceOf(eth.accounts[8])

Step 6.2: Supply Controller decreases supply by 8,000 PAX. Note the balance of Controller and Total Supply.

> tokenContract.balanceOf(eth.accounts[8])
> tokenContract.totalSupply()
> tokenContract.decreaseSupply(8000, {from:eth.accounts[8]})> tokenContract.balanceOf(eth.accounts[8])
> tokenContract.totalSupply()

We will see the amount 8000 decreased is directly from Supply Controller. The Total Supply is also reduced by that amount.

This process is done by decreaseSupply(). You can see how it is implemented in contract code (line 373–374).

Summary

In this article we have seen a live ERC20 contract (Paxos Standard, PAX). Besides the standard ERC20 functions, they include additional functions to meet their specific needs (token minting and burning, and law enforcement). We also deploy this contract onto our Ethereum simulator and simulate some operations on this contract.

Appendix

PAXImplementation Contract Code (source: link)

ABI for this Contract Code

[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"lawEnforcementRole","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"supplyController","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"oldOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"addr","type":"address"}],"name":"AddressFrozen","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"addr","type":"address"}],"name":"AddressUnfrozen","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"addr","type":"address"}],"name":"FrozenAddressWiped","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"oldLawEnforcementRole","type":"address"},{"indexed":true,"name":"newLawEnforcementRole","type":"address"}],"name":"LawEnforcementRoleSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"SupplyIncreased","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"SupplyDecreased","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"oldSupplyController","type":"address"},{"indexed":true,"name":"newSupplyController","type":"address"}],"name":"SupplyControllerSet","type":"event"},{"constant":false,"inputs":[],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_newLawEnforcementRole","type":"address"}],"name":"setLawEnforcementRole","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"freeze","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"unfreeze","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"wipeFrozenAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"isFrozen","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newSupplyController","type":"address"}],"name":"setSupplyController","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"increaseSupply","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"decreaseSupply","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]

--

--