├── .gitignore ├── contracts ├── MyLegacyToken.sol ├── MyUpgradeableToken.sol └── lib │ └── ERC20Standard.sol ├── migrations └── .gitkeep ├── package-lock.json ├── package.json ├── readme.md ├── test └── contracts │ ├── MyLegacyToken.test.js │ ├── MyUpgradeableToken.test.js │ └── behaviors │ ├── ERC20.behavior.js │ └── ERC20Detailed.behavior.js └── truffle-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | 4 | .zos.session 5 | -------------------------------------------------------------------------------- /contracts/MyLegacyToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | import "./lib/ERC20Standard.sol"; 5 | 6 | contract MyLegacyToken is ERC20Standard { 7 | uint8 private constant DECIMALS = 18; 8 | string private constant NAME = "My Legacy Token"; 9 | string private constant SYMBOL = "MLT"; 10 | 11 | constructor () ERC20Standard(NAME, SYMBOL, DECIMALS) public { 12 | uint256 initialSupply = 10000 * (10 ** uint256(DECIMALS)); 13 | _mint(msg.sender, initialSupply); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/MyUpgradeableToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "zos-lib/contracts/Initializable.sol"; 4 | import "openzeppelin-eth/contracts/token/ERC20/ERC20.sol"; 5 | import "openzeppelin-eth/contracts/token/ERC20/IERC20.sol"; 6 | import "openzeppelin-eth/contracts/drafts/ERC20Migrator.sol"; 7 | import "openzeppelin-eth/contracts/token/ERC20/ERC20Mintable.sol"; 8 | import "openzeppelin-eth/contracts/token/ERC20/ERC20Detailed.sol"; 9 | 10 | /** 11 | * @title MyUpgradeableToken 12 | * @dev This contract is an upgradeable ERC20 token example to show how a regular token could be migrated using 13 | * ZeppelinOS and the ERC20Migrator contract provided by the EVM package openzeppelin-eth. 14 | */ 15 | contract MyUpgradeableToken is Initializable, ERC20Detailed, ERC20Mintable { 16 | 17 | /** 18 | * @dev Initialization function. 19 | * @dev This function will initialize the new upgradeable ERC20 contract and will set up the ERC20 migrator. 20 | */ 21 | function initialize(ERC20Detailed _legacyToken, ERC20Migrator _migrator) initializer public { 22 | ERC20Mintable.initialize(address(_migrator)); 23 | ERC20Detailed.initialize(_legacyToken.name(), _legacyToken.symbol(), _legacyToken.decimals()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /contracts/lib/ERC20Standard.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | import "openzeppelin-eth/contracts/math/SafeMath.sol"; 5 | import "openzeppelin-eth/contracts/token/ERC20/IERC20.sol"; 6 | 7 | contract ERC20Standard is IERC20 { 8 | using SafeMath for uint256; 9 | 10 | string private _name; 11 | string private _symbol; 12 | uint8 private _decimals; 13 | uint256 private _totalSupply; 14 | mapping (address => uint256) private _balances; 15 | mapping (address => mapping (address => uint256)) private _allowed; 16 | 17 | constructor(string memory name, string memory symbol, uint8 decimals) public { 18 | _name = name; 19 | _symbol = symbol; 20 | _decimals = decimals; 21 | } 22 | 23 | /** 24 | * @return the name of the token. 25 | */ 26 | function name() public view returns(string memory) { 27 | return _name; 28 | } 29 | 30 | /** 31 | * @return the symbol of the token. 32 | */ 33 | function symbol() public view returns(string memory) { 34 | return _symbol; 35 | } 36 | 37 | /** 38 | * @return the number of decimals of the token. 39 | */ 40 | function decimals() public view returns(uint8) { 41 | return _decimals; 42 | } 43 | 44 | /** 45 | * @dev Total number of tokens in existence 46 | */ 47 | function totalSupply() public view returns (uint256) { 48 | return _totalSupply; 49 | } 50 | 51 | /** 52 | * @dev Gets the balance of the specified address. 53 | * @param owner The address to query the balance of. 54 | * @return An uint256 representing the amount owned by the passed address. 55 | */ 56 | function balanceOf(address owner) public view returns (uint256) { 57 | return _balances[owner]; 58 | } 59 | 60 | /** 61 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 62 | * @param owner address The address which owns the funds. 63 | * @param spender address The address which will spend the funds. 64 | * @return A uint256 specifying the amount of tokens still available for the spender. 65 | */ 66 | function allowance( 67 | address owner, 68 | address spender 69 | ) 70 | public 71 | view 72 | returns (uint256) 73 | { 74 | return _allowed[owner][spender]; 75 | } 76 | 77 | /** 78 | * @dev Transfer token for a specified address 79 | * @param to The address to transfer to. 80 | * @param value The amount to be transferred. 81 | */ 82 | function transfer(address to, uint256 value) public returns (bool) { 83 | _transfer(msg.sender, to, value); 84 | return true; 85 | } 86 | 87 | /** 88 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 89 | * Beware that changing an allowance with this method brings the risk that someone may use both the old 90 | * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 91 | * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 92 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 93 | * @param spender The address which will spend the funds. 94 | * @param value The amount of tokens to be spent. 95 | */ 96 | function approve(address spender, uint256 value) public returns (bool) { 97 | require(spender != address(0)); 98 | 99 | _allowed[msg.sender][spender] = value; 100 | emit Approval(msg.sender, spender, value); 101 | return true; 102 | } 103 | 104 | /** 105 | * @dev Transfer tokens from one address to another 106 | * @param from address The address which you want to send tokens from 107 | * @param to address The address which you want to transfer to 108 | * @param value uint256 the amount of tokens to be transferred 109 | */ 110 | function transferFrom( 111 | address from, 112 | address to, 113 | uint256 value 114 | ) 115 | public 116 | returns (bool) 117 | { 118 | require(value <= _allowed[from][msg.sender]); 119 | 120 | _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value); 121 | _transfer(from, to, value); 122 | return true; 123 | } 124 | 125 | /** 126 | * @dev Increase the amount of tokens that an owner allowed to a spender. 127 | * approve should be called when allowed_[_spender] == 0. To increment 128 | * allowed value is better to use this function to avoid 2 calls (and wait until 129 | * the first transaction is mined) 130 | * From MonolithDAO Token.sol 131 | * @param spender The address which will spend the funds. 132 | * @param addedValue The amount of tokens to increase the allowance by. 133 | */ 134 | function increaseAllowance( 135 | address spender, 136 | uint256 addedValue 137 | ) 138 | public 139 | returns (bool) 140 | { 141 | require(spender != address(0)); 142 | 143 | _allowed[msg.sender][spender] = ( 144 | _allowed[msg.sender][spender].add(addedValue)); 145 | emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); 146 | return true; 147 | } 148 | 149 | /** 150 | * @dev Decrease the amount of tokens that an owner allowed to a spender. 151 | * approve should be called when allowed_[_spender] == 0. To decrement 152 | * allowed value is better to use this function to avoid 2 calls (and wait until 153 | * the first transaction is mined) 154 | * From MonolithDAO Token.sol 155 | * @param spender The address which will spend the funds. 156 | * @param subtractedValue The amount of tokens to decrease the allowance by. 157 | */ 158 | function decreaseAllowance( 159 | address spender, 160 | uint256 subtractedValue 161 | ) 162 | public 163 | returns (bool) 164 | { 165 | require(spender != address(0)); 166 | 167 | _allowed[msg.sender][spender] = ( 168 | _allowed[msg.sender][spender].sub(subtractedValue)); 169 | emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); 170 | return true; 171 | } 172 | 173 | /** 174 | * @dev Transfer token for a specified addresses 175 | * @param from The address to transfer from. 176 | * @param to The address to transfer to. 177 | * @param value The amount to be transferred. 178 | */ 179 | function _transfer(address from, address to, uint256 value) internal { 180 | require(value <= _balances[from]); 181 | require(to != address(0)); 182 | 183 | _balances[from] = _balances[from].sub(value); 184 | _balances[to] = _balances[to].add(value); 185 | emit Transfer(from, to, value); 186 | } 187 | 188 | /** 189 | * @dev Internal function that mints an amount of the token and assigns it to 190 | * an account. This encapsulates the modification of balances such that the 191 | * proper events are emitted. 192 | * @param account The account that will receive the created tokens. 193 | * @param value The amount that will be created. 194 | */ 195 | function _mint(address account, uint256 value) internal { 196 | require(account != address(0)); 197 | _totalSupply = _totalSupply.add(value); 198 | _balances[account] = _balances[account].add(value); 199 | emit Transfer(address(0), account, value); 200 | } 201 | 202 | /** 203 | * @dev Internal function that burns an amount of the token of a given 204 | * account. 205 | * @param account The account whose tokens will be burnt. 206 | * @param value The amount that will be burnt. 207 | */ 208 | function _burn(address account, uint256 value) internal { 209 | require(account != address(0)); 210 | require(value <= _balances[account]); 211 | 212 | _totalSupply = _totalSupply.sub(value); 213 | _balances[account] = _balances[account].sub(value); 214 | emit Transfer(account, address(0), value); 215 | } 216 | 217 | /** 218 | * @dev Internal function that burns an amount of the token of a given 219 | * account, deducting from the sender's allowance for said account. Uses the 220 | * internal burn function. 221 | * @param account The account whose tokens will be burnt. 222 | * @param value The amount that will be burnt. 223 | */ 224 | function _burnFrom(address account, uint256 value) internal { 225 | require(value <= _allowed[account][msg.sender]); 226 | 227 | // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted, 228 | // this function needs to emit an event with the updated approval. 229 | _allowed[account][msg.sender] = _allowed[account][msg.sender].sub( 230 | value); 231 | _burn(account, value); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | null -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erc20-opt-in-onboarding", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Facu Spagnuolo ", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "NODE_ENV=test npx truffle test" 9 | }, 10 | "dependencies": { 11 | "openzeppelin-eth": "^2.1.3", 12 | "truffle": "^5.0.3", 13 | "zos": "^2.2.0-rc.1", 14 | "zos-lib": "^2.2.0-rc.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > :warning: OpenZeppelin SDK is no longer being developed. 2 | > 3 | > For smart contract upgrades we recommend using [OpenZeppelin Upgrades Plugins](https://docs.openzeppelin.com/upgrades-plugins/). 4 | 5 | # ERC20 opt-in onboarding 6 | 7 | **This code uses an old version of the OpenZeppelin SDK, named ZeppelinOS. Nonetheless, the instructions in the documentation still apply.** 8 | 9 | This is a sample project illustrating how a regular ERC20 token can be migrated to an upgradeable ERC20 using the OpenZeppelin SDK. Please follow our [official documentation](https://docs.zeppelinos.org/docs/erc20_onboarding.html) to better understand how to onboard your token to the OpenZeppelin SDK. 10 | -------------------------------------------------------------------------------- /test/contracts/MyLegacyToken.test.js: -------------------------------------------------------------------------------- 1 | const { Contracts } = require('zos-lib') 2 | const shouldBehaveLikeERC20Detailed = require('./behaviors/ERC20Detailed.behavior') 3 | const shouldBehaveLikeERC20 = require('./behaviors/ERC20.behavior') 4 | 5 | const MyLegacyToken = Contracts.getFromLocal('MyLegacyToken') 6 | 7 | contract('MyLegacyToken', function ([_, owner, recipient, anotherAccount]) { 8 | const initialSupply = new web3.BigNumber('10000e18') 9 | 10 | beforeEach('deploying token', async function () { 11 | this.token = await MyLegacyToken.new({ from: owner }) 12 | }) 13 | 14 | shouldBehaveLikeERC20([owner, recipient, anotherAccount], initialSupply) 15 | shouldBehaveLikeERC20Detailed('My Legacy Token', 'MLT', 18, initialSupply) 16 | }) 17 | -------------------------------------------------------------------------------- /test/contracts/MyUpgradeableToken.test.js: -------------------------------------------------------------------------------- 1 | const { Contracts, encodeCall, assertRevert } = require('zos-lib') 2 | const shouldBehaveLikeERC20 = require('./behaviors/ERC20.behavior') 3 | const shouldBehaveLikeERC20Detailed = require('./behaviors/ERC20Detailed.behavior') 4 | 5 | const MyLegacyToken = Contracts.getFromLocal('MyLegacyToken') 6 | const MyUpgradeableToken = Contracts.getFromLocal('MyUpgradeableToken') 7 | const ERC20Migrator = Contracts.getFromNodeModules('openzeppelin-eth', 'ERC20Migrator') 8 | 9 | contract('MyUpgradeableToken', function ([_, owner, recipient, anotherAccount]) { 10 | const name = 'My Legacy Token' 11 | const symbol = 'MLT' 12 | const decimals = 18 13 | 14 | beforeEach('deploying legacy and upgradeable tokens', async function () { 15 | this.legacyToken = await MyLegacyToken.new({ from: owner }) 16 | 17 | this.migrator = await ERC20Migrator.new() 18 | const migratorData = encodeCall('initialize', ['address'], [this.legacyToken.address]) 19 | await this.migrator.sendTransaction({ data: migratorData }) 20 | 21 | this.upgradeableToken = await MyUpgradeableToken.new() 22 | const upgradeableTokenData = encodeCall('initialize', ['address', 'address'], [this.legacyToken.address, this.migrator.address]) 23 | await this.upgradeableToken.sendTransaction({ data: upgradeableTokenData }) 24 | 25 | await this.migrator.beginMigration(this.upgradeableToken.address); 26 | }) 27 | 28 | describe('ERC20 token behavior', function () { 29 | const initialSupply = new web3.BigNumber('10000e18') 30 | 31 | beforeEach('migrating balance to new token', async function () { 32 | await this.legacyToken.approve(this.migrator.address, initialSupply, { from: owner }) 33 | await this.migrator.migrate(owner, initialSupply) 34 | this.token = this.upgradeableToken 35 | }) 36 | 37 | shouldBehaveLikeERC20([owner, recipient, anotherAccount], initialSupply) 38 | shouldBehaveLikeERC20Detailed(name, symbol, decimals) 39 | }) 40 | 41 | describe('migrate', function () { 42 | beforeEach('approving 50 tokens to the new contract', async function () { 43 | await this.legacyToken.approve(this.migrator.address, 50, { from: owner }) 44 | }) 45 | 46 | describe('when the amount is lower or equal to the one approved', function () { 47 | const amount = 50 48 | 49 | it('mints that amount of the new token', async function () { 50 | await this.migrator.migrate(owner, amount) 51 | 52 | const currentBalance = await this.upgradeableToken.balanceOf(owner) 53 | assert(currentBalance.eq(amount)) 54 | }) 55 | 56 | it('transfers given amount of old tokens to the migrator', async function () { 57 | await this.migrator.migrate(owner, amount) 58 | 59 | const currentMigratorBalance = await this.legacyToken.balanceOf(this.migrator.address) 60 | assert(currentMigratorBalance.eq(amount)) 61 | }) 62 | 63 | it('updates the total supply', async function () { 64 | await this.migrator.migrate(owner, amount) 65 | 66 | const currentSupply = await this.upgradeableToken.totalSupply() 67 | assert(currentSupply.eq(amount)) 68 | }) 69 | }) 70 | 71 | describe('when the given amount is higher than the one approved', function () { 72 | const amount = 51 73 | 74 | it('reverts', async function () { 75 | await assertRevert(this.migrator.migrate(owner, amount)) 76 | }) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/contracts/behaviors/ERC20.behavior.js: -------------------------------------------------------------------------------- 1 | const { assertRevert } = require('zos-lib') 2 | 3 | module.exports = function ([owner, recipient, anotherAccount], initialSupply) { 4 | const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 5 | 6 | describe('total supply', function () { 7 | it('returns the total amount of tokens', async function () { 8 | const totalSupply = await this.token.totalSupply() 9 | 10 | assert(totalSupply.eq(initialSupply)) 11 | }) 12 | }) 13 | 14 | describe('balanceOf', function () { 15 | describe('when the requested account has no tokens', function () { 16 | it('returns zero', async function () { 17 | const balance = await this.token.balanceOf(anotherAccount) 18 | 19 | assert(balance.eq(0)) 20 | }) 21 | }) 22 | 23 | describe('when the requested account has some tokens', function () { 24 | it('returns the total amount of tokens', async function () { 25 | const balance = await this.token.balanceOf(owner) 26 | 27 | assert(balance.eq(initialSupply)) 28 | }) 29 | }) 30 | }) 31 | 32 | describe('transfer', function () { 33 | describe('when the recipient is not the zero address', function () { 34 | const to = recipient 35 | 36 | describe('when the sender does not have enough balance', function () { 37 | const amount = initialSupply.add(1) 38 | 39 | it('reverts', async function () { 40 | await assertRevert(this.token.transfer(to, amount, { from: owner })) 41 | }) 42 | }) 43 | 44 | describe('when the sender has enough balance', function () { 45 | const amount = initialSupply 46 | 47 | it('transfers the requested amount', async function () { 48 | await this.token.transfer(to, amount, { from: owner }) 49 | 50 | const senderBalance = await this.token.balanceOf(owner) 51 | assert(senderBalance.eq(0)) 52 | 53 | const recipientBalance = await this.token.balanceOf(to) 54 | assert(recipientBalance.eq(amount)) 55 | }) 56 | 57 | it('emits a transfer event', async function () { 58 | const { logs } = await this.token.transfer(to, amount, { from: owner }) 59 | 60 | assert.equal(logs.length, 1) 61 | assert.equal(logs[0].event, 'Transfer') 62 | assert.equal(logs[0].args.from, owner) 63 | assert.equal(logs[0].args.to, to) 64 | assert(logs[0].args.value.eq(amount)) 65 | }) 66 | }) 67 | }) 68 | 69 | describe('when the recipient is the zero address', function () { 70 | const to = ZERO_ADDRESS 71 | 72 | it('reverts', async function () { 73 | await assertRevert(this.token.transfer(to, initialSupply, { from: owner })) 74 | }) 75 | }) 76 | }) 77 | 78 | describe('approve', function () { 79 | describe('when the spender is not the zero address', function () { 80 | const spender = recipient 81 | 82 | describe('when the sender has enough balance', function () { 83 | const amount = initialSupply 84 | 85 | it('emits an approval event', async function () { 86 | const { logs } = await this.token.approve(spender, amount, { from: owner }) 87 | 88 | assert.equal(logs.length, 1) 89 | assert.equal(logs[0].event, 'Approval') 90 | assert.equal(logs[0].args.owner, owner) 91 | assert.equal(logs[0].args.spender, spender) 92 | assert(logs[0].args.value.eq(amount)) 93 | }) 94 | 95 | describe('when there was no approved amount before', function () { 96 | it('approves the requested amount', async function () { 97 | await this.token.approve(spender, amount, { from: owner }) 98 | 99 | const allowance = await this.token.allowance(owner, spender) 100 | assert(allowance.eq(amount)) 101 | }) 102 | }) 103 | 104 | describe('when the spender had an approved amount', function () { 105 | beforeEach(async function () { 106 | await this.token.approve(spender, 1, { from: owner }) 107 | }) 108 | 109 | it('approves the requested amount and replaces the previous one', async function () { 110 | await this.token.approve(spender, amount, { from: owner }) 111 | 112 | const allowance = await this.token.allowance(owner, spender) 113 | assert(allowance.eq(amount)) 114 | }) 115 | }) 116 | }) 117 | 118 | describe('when the sender does not have enough balance', function () { 119 | const amount = initialSupply.add(1) 120 | 121 | it('emits an approval event', async function () { 122 | const { logs } = await this.token.approve(spender, amount, { from: owner }) 123 | 124 | assert.equal(logs.length, 1) 125 | assert.equal(logs[0].event, 'Approval') 126 | assert.equal(logs[0].args.owner, owner) 127 | assert.equal(logs[0].args.spender, spender) 128 | assert(logs[0].args.value.eq(amount)) 129 | }) 130 | 131 | describe('when there was no approved amount before', function () { 132 | it('approves the requested amount', async function () { 133 | await this.token.approve(spender, amount, { from: owner }) 134 | 135 | const allowance = await this.token.allowance(owner, spender) 136 | assert(allowance.eq(amount)) 137 | }) 138 | }) 139 | 140 | describe('when the spender had an approved amount', function () { 141 | beforeEach(async function () { 142 | await this.token.approve(spender, 1, { from: owner }) 143 | }) 144 | 145 | it('approves the requested amount and replaces the previous one', async function () { 146 | await this.token.approve(spender, amount, { from: owner }) 147 | 148 | const allowance = await this.token.allowance(owner, spender) 149 | assert(allowance.eq(amount)) 150 | }) 151 | }) 152 | }) 153 | }) 154 | 155 | describe('when the spender is the zero address', function () { 156 | const amount = initialSupply 157 | const spender = ZERO_ADDRESS 158 | 159 | it('reverts', async function () { 160 | await assertRevert(this.token.approve(spender, amount, { from: owner })) 161 | }) 162 | }) 163 | }) 164 | 165 | describe('transfer from', function () { 166 | const spender = recipient 167 | 168 | describe('when the recipient is not the zero address', function () { 169 | const to = anotherAccount 170 | 171 | describe('when the spender has enough approved balance', function () { 172 | beforeEach(async function () { 173 | await this.token.approve(spender, initialSupply, { from: owner }) 174 | }) 175 | 176 | describe('when the owner has enough balance', function () { 177 | const amount = initialSupply 178 | 179 | it('transfers the requested amount', async function () { 180 | await this.token.transferFrom(owner, to, amount, { from: spender }) 181 | 182 | const senderBalance = await this.token.balanceOf(owner) 183 | assert(senderBalance.eq(0)) 184 | 185 | const recipientBalance = await this.token.balanceOf(to) 186 | assert(recipientBalance.eq(amount)) 187 | }) 188 | 189 | it('decreases the spender allowance', async function () { 190 | await this.token.transferFrom(owner, to, amount, { from: spender }) 191 | 192 | const allowance = await this.token.allowance(owner, spender) 193 | assert(allowance.eq(0)) 194 | }) 195 | 196 | it('emits a transfer event', async function () { 197 | const { logs } = await this.token.transferFrom(owner, to, amount, { from: spender }) 198 | 199 | assert.equal(logs.length, 1) 200 | assert.equal(logs[0].event, 'Transfer') 201 | assert.equal(logs[0].args.from, owner) 202 | assert.equal(logs[0].args.to, to) 203 | assert(logs[0].args.value.eq(amount)) 204 | }) 205 | }) 206 | 207 | describe('when the owner does not have enough balance', function () { 208 | const amount = initialSupply.add(1) 209 | 210 | it('reverts', async function () { 211 | await assertRevert(this.token.transferFrom(owner, to, amount, { from: spender })) 212 | }) 213 | }) 214 | }) 215 | 216 | describe('when the spender does not have enough approved balance', function () { 217 | beforeEach(async function () { 218 | await this.token.approve(spender, initialSupply.minus(1), { from: owner }) 219 | }) 220 | 221 | describe('when the owner has enough balance', function () { 222 | const amount = initialSupply 223 | 224 | it('reverts', async function () { 225 | await assertRevert(this.token.transferFrom(owner, to, amount, { from: spender })) 226 | }) 227 | }) 228 | 229 | describe('when the owner does not have enough balance', function () { 230 | const amount = initialSupply.add(1) 231 | 232 | it('reverts', async function () { 233 | await assertRevert(this.token.transferFrom(owner, to, amount, { from: spender })) 234 | }) 235 | }) 236 | }) 237 | }) 238 | 239 | describe('when the recipient is the zero address', function () { 240 | const amount = initialSupply 241 | const to = ZERO_ADDRESS 242 | 243 | beforeEach(async function () { 244 | await this.token.approve(spender, amount, { from: owner }) 245 | }) 246 | 247 | it('reverts', async function () { 248 | await assertRevert(this.token.transferFrom(owner, to, amount, { from: spender })) 249 | }) 250 | }) 251 | }) 252 | 253 | describe('decrease approval', function () { 254 | describe('when the spender is not the zero address', function () { 255 | const spender = recipient 256 | 257 | describe('when the sender has enough balance', function () { 258 | const amount = initialSupply 259 | 260 | describe('when there was no approved amount before', function () { 261 | it('reverts', async function () { 262 | await assertRevert(this.token.decreaseAllowance(spender, amount, { from: owner })) 263 | }) 264 | }) 265 | 266 | describe('when the spender had an approved amount', function () { 267 | beforeEach(async function () { 268 | await this.token.approve(spender, amount.add(1), { from: owner }) 269 | }) 270 | 271 | it('emits an approval event', async function () { 272 | const { logs } = await this.token.decreaseAllowance(spender, amount, { from: owner }) 273 | 274 | assert.equal(logs.length, 1) 275 | assert.equal(logs[0].event, 'Approval') 276 | assert.equal(logs[0].args.owner, owner) 277 | assert.equal(logs[0].args.spender, spender) 278 | assert(logs[0].args.value.eq(1)) 279 | }) 280 | 281 | it('decreases the spender allowance subtracting the requested amount', async function () { 282 | await this.token.decreaseAllowance(spender, amount, { from: owner }) 283 | 284 | const allowance = await this.token.allowance(owner, spender) 285 | assert(allowance.eq(1)) 286 | }) 287 | }) 288 | }) 289 | 290 | describe('when the sender does not have enough balance', function () { 291 | const amount = initialSupply.add(1) 292 | 293 | describe('when there was no approved amount before', function () { 294 | it('reverts', async function () { 295 | await assertRevert(this.token.decreaseAllowance(spender, amount, { from: owner })) 296 | }) 297 | }) 298 | 299 | describe('when the spender had an approved amount', function () { 300 | beforeEach(async function () { 301 | await this.token.approve(spender, amount.add(1), { from: owner }) 302 | }) 303 | 304 | it('emits an approval event', async function () { 305 | const { logs } = await this.token.decreaseAllowance(spender, amount, { from: owner }) 306 | 307 | assert.equal(logs.length, 1) 308 | assert.equal(logs[0].event, 'Approval') 309 | assert.equal(logs[0].args.owner, owner) 310 | assert.equal(logs[0].args.spender, spender) 311 | assert(logs[0].args.value.eq(1)) 312 | }) 313 | 314 | it('decreases the spender allowance subtracting the requested amount', async function () { 315 | await this.token.decreaseAllowance(spender, amount, { from: owner }) 316 | 317 | const allowance = await this.token.allowance(owner, spender) 318 | assert(allowance.eq(1)) 319 | }) 320 | }) 321 | }) 322 | }) 323 | 324 | describe('when the spender is the zero address', function () { 325 | const amount = initialSupply 326 | const spender = ZERO_ADDRESS 327 | 328 | it('reverts', async function () { 329 | await assertRevert(this.token.decreaseAllowance(spender, amount, { from: owner })) 330 | }) 331 | }) 332 | }) 333 | 334 | describe('increase approval', function () { 335 | const amount = initialSupply 336 | 337 | describe('when the spender is not the zero address', function () { 338 | const spender = recipient 339 | 340 | describe('when the sender has enough balance', function () { 341 | it('emits an approval event', async function () { 342 | const { logs } = await this.token.increaseAllowance(spender, amount, { from: owner }) 343 | 344 | assert.equal(logs.length, 1) 345 | assert.equal(logs[0].event, 'Approval') 346 | assert.equal(logs[0].args.owner, owner) 347 | assert.equal(logs[0].args.spender, spender) 348 | assert(logs[0].args.value.eq(amount)) 349 | }) 350 | 351 | describe('when there was no approved amount before', function () { 352 | it('approves the requested amount', async function () { 353 | await this.token.increaseAllowance(spender, amount, { from: owner }) 354 | 355 | const allowance = await this.token.allowance(owner, spender) 356 | assert(allowance.eq(amount)) 357 | }) 358 | }) 359 | 360 | describe('when the spender had an approved amount', function () { 361 | beforeEach(async function () { 362 | await this.token.approve(spender, 1, { from: owner }) 363 | }) 364 | 365 | it('increases the spender allowance adding the requested amount', async function () { 366 | await this.token.increaseAllowance(spender, amount, { from: owner }) 367 | 368 | const allowance = await this.token.allowance(owner, spender) 369 | assert(allowance.eq(amount.add(1))) 370 | }) 371 | }) 372 | }) 373 | 374 | describe('when the sender does not have enough balance', function () { 375 | const amount = initialSupply.add(1) 376 | 377 | it('emits an approval event', async function () { 378 | const { logs } = await this.token.increaseAllowance(spender, amount, { from: owner }) 379 | 380 | assert.equal(logs.length, 1) 381 | assert.equal(logs[0].event, 'Approval') 382 | assert.equal(logs[0].args.owner, owner) 383 | assert.equal(logs[0].args.spender, spender) 384 | assert(logs[0].args.value.eq(amount)) 385 | }) 386 | 387 | describe('when there was no approved amount before', function () { 388 | it('approves the requested amount', async function () { 389 | await this.token.increaseAllowance(spender, amount, { from: owner }) 390 | 391 | const allowance = await this.token.allowance(owner, spender) 392 | assert(allowance.eq(amount)) 393 | }) 394 | }) 395 | 396 | describe('when the spender had an approved amount', function () { 397 | beforeEach(async function () { 398 | await this.token.approve(spender, 1, { from: owner }) 399 | }) 400 | 401 | it('increases the spender allowance adding the requested amount', async function () { 402 | await this.token.increaseAllowance(spender, amount, { from: owner }) 403 | 404 | const allowance = await this.token.allowance(owner, spender) 405 | assert(allowance.eq(amount.add(1))) 406 | }) 407 | }) 408 | }) 409 | }) 410 | 411 | describe('when the spender is the zero address', function () { 412 | const spender = ZERO_ADDRESS 413 | 414 | it('reverts', async function () { 415 | await assertRevert(this.token.increaseAllowance(spender, amount, { from: owner })) 416 | }) 417 | }) 418 | }) 419 | } 420 | -------------------------------------------------------------------------------- /test/contracts/behaviors/ERC20Detailed.behavior.js: -------------------------------------------------------------------------------- 1 | module.exports = function (_name, _symbol, _decimals) { 2 | it('has a name', async function () { 3 | const name = await this.token.name(); 4 | assert.equal(name, _name); 5 | }); 6 | 7 | it('has a symbol', async function () { 8 | const symbol = await this.token.symbol(); 9 | assert.equal(symbol, _symbol); 10 | }); 11 | 12 | it('has an amount of decimals', async function () { 13 | const decimals = await this.token.decimals(); 14 | assert(decimals.eq(_decimals)); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | networks: { 5 | local: { 6 | host: 'localhost', 7 | port: 9545, 8 | gas: 5000000, 9 | gasPrice: 5e9, 10 | network_id: '*' 11 | } 12 | } 13 | }; --------------------------------------------------------------------------------