This is a Dawn ERC-20 token for FirstBlood decentralised eSports platform. Dawn is a global, self-governed, and open-sourced protocol that empowers and rewards gamers.
- Introduction
- Software required
- Overview
- Base packages
- Testing
- Conformance suite
- Security discussion
- Linting
- Deploying
- Deployments
- Audits
- Other
-
Dawn token (DAWN) is a new token that is 1:1 swapped from the existing FirstBlood 1ST token.
-
Token swap requires an identity verification that is done on the server-side whitelisting, using a Solidity
ecrecover()
signing. -
DAWN can be used on FirstBlood platform and other services as a utility token: method of payment, staking, etc.
-
Token complies with EIP-20 standard (former ERC-20 candidate). The original 1ST token was created at the time when ERC-20 process was still about to start, so some implementation details are different. This makes it easier to use token in various decentralised finance services like decentralised exchanages (DEXes) and lending pools.
-
Token complies with EIP-777 standard (former ERC-777 candidate). This standard defines a new way to interact with a token contract while remaining backward compatible with ERC20.
-
Token smart contract supports recovering tokens accidentally send into it.
-
The token is upgradeable through OpenZeppelin proxy pattern (actual contracts).
-
The token implements a pause functionality that will be activated if and when the tokens are migrated to a new network. This is implemented using OpenZeppelin pausable trait. This is similar what EOS did with their genesis block. Furthermore, the pause can be activated in the case there is a large exchange hack and a large token supply is lost, so that other exchanges have time to update their blacklists.
-
EIP-777 standard supports burning of tokens to allow new utility models in the future.
-
EIP-777 standard allows better token UX when integrating with decentralised finance smart contracts like lending pools.
-
solc 0.5.16 (0.5.16+commit.9c3226ce.Emscripten.clang)
-
Node 0.12
-
openzeppelin-solidity 2.5.0
Below is a narrative introduction to the Dawn token and related smart contracts.
All contracts are based on base contracts from OpenZeppelin SDK package.
This package differs from more well known openzeppelin-solidity
package, as all contracts use Initializer
pattern instead of constructor
pattern to set up the contracts.
This is done to support Upgrade Proxies that do not work with direct constructors.
Although, currently only Dawn ERC-20 token contracts is upgradeable, we do not want to mix contracts with the same name from two different packages, so all contracts are done using OpenZeppelin SDK flavour.
The token is based on [OpenZeppelin upgradeable ERC-20 contract].
The upgrade proxy contract is the actual "published" token contract.
-
Upgrade proxy will be controlled by its own multisig, as
owner()
of token cannot be shared between proxy and the actual implementation -
Token owner multisig can pause the token - an action that might later required in a chain migration
-
Token supports the following traits: Burnable, Pausable, Recoverable
The token swap contract is written from the scratch for this swap.
-
The new token supply is held in the owner multisig and approve()'ed for the token swap
-
The users will
approve()
their old tokens for the swap and then perform aswap()
transaction -
Each
swap()
transaction requires a server-side calculation of v, r, and s. We use this withecrecover()
to check that the address performing the swap was whitelisted by our server. -
Token swap contract has
Recoverable
trait to help users with the issues of missending wrong tokens on the contract -
Token swap contract has a
burn()
with a destination, where the old tokens can be send to die by the swap owner. The legacy 1ST token does not have a burn trait, so these tokens are simply send to the zero address.
A staking contract allows users to lock up a predefined amount of tokens for a predefined time. This is the simplest form of staking, allowing FirstBlood and other eSports actors to take the first steps towards a more decentralized ecosystem over time.
Staking happens by doing ERC-777 send() in to the staking contract. The send amount, or price, is predefined, but can be adjusted over a time by an oracle. Likewise, the lock period is predefined but can be adjusted.
To unstake, the user must manually call unstake()
for their
previous stake id.
The staking contract also supports "flash staking", so that tokens are bought and staked in a single transaction, instead of the user need to go through manual steps to set everything accept.
-
There could be a smart contract that accepts ETH payments
-
The smart contract buys DAWN token with this ETH from a liquidity pool like Uniswap
-
Bought tokens are staked on the behalf of the user by the smart contract
-
Extra tokens are send back to the wallet that paid ETH for buying in the first place
To stake on the behalf of someone else,
you need to fill userData
of ERC-777 send()
with correct parameters. This message will be decoded
by ERC777TokensRecipient callback.
-
Staking contract is in-house, written from the scratch.
-
Users can stake their tokens. Each stake needs the exact amount of tokens defined by the staking contract. The user must assign a stake id for their staking, and the stake is referred by this id in the events and unstaking.
-
Later, after the stake period is expired, the user can
unstake()
their tokens using the given id. -
User can run multiple stakes a the same time, though there is no real use case for this.
-
Staking contract has
Recoverable
trait to help users with the issues of missending wrong tokens on the contract. -
There is a special account,
stakingOracle
, that can set the stake amount and period. This gives flexibility e.g. to automate staking amount to follow a predefined US dollar price in the future.
Recoverable is a mix-in contract that allows the owner to reclaim any ERC-20 tokens send on the contract.
The token faucet contract is a simple test token give away contract. Its intend is to give people tokens in the (Görli) testnet, so they can interact with the token swap on the beta website.
You need to generate ABI files in build/
npx truffle compile
Then you can run tests with Jest:
npx jest
Example:
npx jest -t 'Proxy should have an admin right'
Example:
npx jest --testPathPattern "tokenswap.ts"
Use CMD + F1
and turn on Debugger Auto Attach in command palette.
Then you can run individual tests and VSCode will attach
node --inspect-brk node_modules/.bin/jest --runInBand -t 'The supply should match original token'
launch.json example:
// https://github.com/microsoft/vscode-recipes/tree/master/debugging-jest-tests
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--runInBand",
"--config",
"./test/jest-e2e.json"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
},
"env": {
"PATH": "/Users/moo/.nvm/versions/node/v11.0.0/bin:${env:PATH}"
},
},
]
}
In conformance folder we have direclty lifted OpenZeppelin ERC-777 test suite. These Truffle tests will test against our proxy token to see that the token does what
Because tests are Truffle, Chai and Mocha, they do not run under our normal test runner. Porting 172 tests for Jest would be little bit too much work. Thus, the tests are put to a separate folder. You can run them with Truffle by:
truffle test conformance/ERC777.js
Proxy and ERC-777 contracts are OpenZeppelin implementations from OpenZeppelin SDK 2.5.
ERC-777 has been modified to make it pausable. Unfortunately OpenZeppelin SDK 2.5 did
not yet support this behavior, so we had to create a contract ERC777Overridable
.
All token transaction facing functions have been made internal, so that
DawnTokenImpl
can add pausable guards around them.
Zeppelin audit report is here.
TokenSwap
and Staking
contracts do not use SafeMath library.
This is because they do not do any kind of accounting math.
The amount of tokens you send in is the amount of tokens you get out.
Thus, the need for SafeMath is cosmetic.
An opportunity for a re-entrance condition exists in Staking
contract
as there is an ERC-777 receiver (tokensReceived) and sender (
unstake`). Both functions have been
guarded with re-entrance guards.
Follow AirBNB TypeScript Coding Conventions
Add to your workspace settings.json
"editor.codeActionsOnSave": {
"source.fixAll": true
}
"prettier.eslintIntegration": true
Add in Visual Studio Code Settings JSON
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": false
}
You can manually format the source code with CMD + F1
and choosing Format document
.
Run eslint
by hand:
npx eslint --ext .ts tests/ src/
This will deploy a mock of old token, a mock of new token, a token swap and a testnet token faucet contracts.
- Generate two new private keys, one for the deployment account and one for the server-side signer
openssl rand -hex 32
-
Create a config file
secrets/goearly.env.ini
. For variable documentation see deployTestnet.js.
deployerPrivateKeyHex = "..."
signerPrivateKeyHex = "..."
oraclePrivateKeyHex = "..."
tokenOwnerPrivateKeyHex = "..."
infuraProjectId = "..."
- Get the address for the deployment account by running the deployer without ETH
npx ts-node src/scripts/deployTestnet.ts
-
Deploy now with having some gas money
npx ts-node src/scripts/deployTestnet.ts
For admin tasks, like approving more tokens, the best way to interact
with the contracts is through ts-node
REPL.
Open ts-node
, start editor mode with .editor
command
and copy-paste in your manipulation script.
Example below:
import { ZWeb3, Contracts } from '@openzeppelin/upgrades';
import { Account } from 'eth-lib/lib';
import { createProider } from './src/utils/deploy';
const OWNER_PRIVATE_KEY = '...'; // No 0x prefix
const SWAP_BUDGET = '500000000000000000000000';
const INFURA_PROJECT_ID = '...';
async function run(): Promise<void> {
// Initialze
const provider = createProvider([OWNER_PRIVATE_KEY], INFURA_PROJECT_ID, 'ropsten');
ZWeb3.initialize(provider);
// Instiate contracts
const TokenFaucet = Contracts.getFromLocal('TokenFaucet');
const FirstBloodTokenMock = Contracts.getFromLocal('FirstBloodTokenMock');
const faucet = TokenFaucet.at('0xC5dec1bB818fbC753D8aFE3aC9275268F8204695');
const oldToken = FirstBloodTokenMock.at('0x9517FC874877A4510A3dE617d0c2517D29E5aD32');
console.log('Connected to', await ZWeb3.getNetworkName(), 'network');
const owner = Account.fromPrivate(`0x${OWNER_PRIVATE_KEY}`).address;
const promise = oldToken.methods.transfer(faucet.address, SWAP_BUDGET.toString()).send({ from: owner });
console.log('Sending transaction');
const receipt = await promise;
console.log('Transaction complete', receipt);
process.exit(0);
}
run();
ABI encoding allows you to input the raw Data
value of Ethereum transaction field,
allowing to manipulate contracts from the wallet that do not have native web3.js support.
One of such wallets is Gnosis Safe multisig wallet 2.0.
Here is an example how to get Data
parameters for an arbitrary contract transaction,
using approve()
as an example.
import { ZWeb3, Contracts } from '@openzeppelin/upgrades';
import { createProvider } from './src/utils/deploy';
const INFURA_PROJECT_ID = '...';
async function run(): Promise<void> {
// Initialze
const provider = createProvider([], INFURA_PROJECT_ID, 'mainnet');
ZWeb3.initialize(provider);
// Instiate contracts
const DawnTokenImpl = Contracts.getFromLocal('DawnTokenImpl');
const TokenSwap = Contracts.getFromLocal('TokenSwap');
const token = DawnTokenImpl.at('0x580c8520dEDA0a441522AEAe0f9F7A5f29629aFa');
const tokenSwap = DawnTokenImpl.at('0x2e776B7BFb8E8307E476BA4B77B21D4532ed47d2');
const holder = '0xedae4cfB12ECfCDE46853f63aBa76D8EA3CF3871';
// Read the full balance of the multisig wallet
const allOfBalance = await token.methods.balanceOf('0xedae4cfB12ECfCDE46853f63aBa76D8EA3CF3871').call();
// Approve this balance to be used for the token swap
const dataPayload = token.methods.approve(tokenSwap.address, allOfBalance).encodeABI();
console.log('Data payload for approve() tx is', dataPayload);
}
run();
DAWN token: 0x580c8520dEDA0a441522AEAe0f9F7A5f29629aFa
Token Swap: 0x2e776B7BFb8E8307E476BA4B77B21D4532ed47d2
Staking: 0x0B7C98Ba6235952BA847209C35189846A1706BC9
1ST token: 0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7
The contracts are currently deployed on Ropsten testnet:
Legacy token {
address: '0x9517FC874877A4510A3dE617d0c2517D29E5aD32',
name: 'Mock of old token',
symbol: 'OLD',
supply: '93468683899196345527500000'
}
Upgrade proxy for new token {
address: '0x59104320Ee5A3bd286AD7017Ad4337d8Bfbd6C45',
admin: '0xbe48960593b6468AF98474996656D2925E1825df',
implementation: '0xB993bE16857dB18fF8A04739654334aDb4164CAE'
}
New token through upgrade proxy {
name: 'Mock of new token',
address: '0x59104320Ee5A3bd286AD7017Ad4337d8Bfbd6C45',
symbol: 'NEW',
supply: '93468683899196345527500000'
}
Token swap {
address: '0x886263505cabBE7312aA3DAEE3d92Fa7D3d0779a',
tokensLeftToSwap: '5000000000000000000000',
signerKey: '39cc67e7dbf2c162095bfc058f4b7ba2f9aa7ec006f9e28dc438c07662a3bb41'
}
Faucet {
address: '0xC5dec1bB818fbC753D8aFE3aC9275268F8204695',
faucetAmount: '300000000000000000000',
balance: '5000000000000000000000'
}
Staking {
address: '0xD031b55e583d842e42b72cf98f834adF78760e73',
token: '0x59104320Ee5A3bd286AD7017Ad4337d8Bfbd6C45',
stakingTime: '86400',
stakingAmount: '2500000000000000000',
oracle: '0x1FCE2cf3D1a1BC980f85FEf6bF3EE17DD6eBcC8D'
}
Here is a sample deployment in Goerli testnet:
Legacy token {
address: '0x14fCbe0B754BF13207f891bc779045F5aAcD16D7',
name: 'Mock of old token',
symbol: 'OLD',
supply: '93468683899196345527500000'
}
Upgrade proxy for new token {
address: '0x353b92d7d5599f855B9162feeBF97d6ea3fA635b',
admin: '0xbe48960593b6468AF98474996656D2925E1825df',
implementation: '0x95Ff51c5978Ca6c510504fb3a97AdaeD223029CD'
}
New token through upgrade proxy {
name: 'Mock of new token',
address: '0x353b92d7d5599f855B9162feeBF97d6ea3fA635b',
symbol: 'NEW',
supply: '93468683899196345527500000'
}
Token swap {
address: '0x8a78b170Aa384Be8473F5dcA92079e1b7b1f7005',
tokensLeftToSwap: '5000000000000000000000',
signerKey: '39cc67e7dbf2c162095bfc058f4b7ba2f9aa7ec006f9e28dc438c07662a3bb41'
}
Faucet {
address: '0x655eE78a11F6D9CB268aB90031C997844c8b60F3',
faucetAmount: '300000000000000000000',
balance: '5000000000000000000000'
}
Staking {
address: '0x960546ece845cc513E8682D3Cc906bc76F1d71Ce',
token: '0x353b92d7d5599f855B9162feeBF97d6ea3fA635b',
stakingTime: '86400',
stakingAmount: '2500000000000000000',
oracle: '0x1FCE2cf3D1a1BC980f85FEf6bF3EE17DD6eBcC8D'
}
-
Truffle and TypeChain example (a legacy reference - was a lot of pain and both Truffle and TypeChain have now been removed)