audit report for clever token · clever token audit report 1 executive summary the following audit...
TRANSCRIPT
Audit report for Clever Token
23/12/2020
21/12/2020
2
Clever Token audit report 1 Executive summary
The following audit report presents the effect of the research that Blockhunters team
conducted on the part of the Clever Token code.
Our audit focused on two parts of the project – ERC20 token mechanisms and distribution
processes, responsible for the business logic of the project.
Blockhunters team has checked the possibility of known Ethereum attacks to be exploited
in the network. Fortunately, the main token contract contains basic functionalities that are not
vulnerable to discovered Ethereum attacks. Clever Token developer has been using SafeMath
libraries that significantly lower the risk of possible miscalculations and errors, although the
implementation of this library was faulty and repaired afterwards. All of the contracts, methods
and state variables were tested and dangerous globalBurn() function was deleted after
exposing it as a potential vulnerability.
Distribution mechanisms were tested and besides their huge complexity, the process
itself can be described as safe. A public primitiveDistribution() function was found, declared as
a vulnerability and then deleted.
1.1 Liability clause
Please note that Blockhunters Company doesn’t verify the economic foundation of the
project but only its code correctness and security issues. We do not take any responsibility for
any misuse or misunderstanding of the information provided and potential economic losses
due to faulty investment decisions. This document doesn’t ensure that the code itself is free
from potential vulnerabilities that were not found. If any questions arise please contact us by
www.blockhunters.io.
1.2 Source code checksums
Before using the smart contracts, please verify the MD5 checksums with the following
ones, which describe the files that were audited.
e8a4df5d1b2da7e4eb4313e734d19e8c ./contracts/CleverProtocol.sol
d86e95e4f61952f4e4c19bacb9cedfa7 ./contracts/CleverToken.sol
354d0a40514c389f0f11f98b092e3b0a ./contracts/TimedSwap.sol
15b15c59f0470c4b214276f4127af763 ./contracts/crowdsale/Crowdsale.sol
8d0efc6e445a760ddd6790c86c187e8f ./contracts/crowdsale/MintedCrowdsale.sol
3
4e99b75d8d54be37c872f137e3251c37 ./contracts/crowdsale/TimedCrowdsale.sol
082a82ed1ab0f52098ef76f9c216994f ./contracts/token/.DS_Store
e249054930e8d626c36e822a7ae74cd3 ./contracts/token/ERC20/.DS_Store
93c501784ad395fb60b279a992d4aba6 ./contracts/token/ERC20/ERC20.sol
d4b36c388a1aeaf8343c0e5545e697ed ./contracts/token/ERC20/ERC20Capped.sol
9842b94e5146a999c4b89c3277d4657a ./contracts/token/ERC20/ERC20Detailed.sol
1b1e299c39f74d8e3f2d38901592eb32 ./contracts/token/ERC20/ERC20Mintable.sol
183ff1f5e5f04bcfa85cee597bcb0236 ./contracts/token/ERC20/IERC20.sol
1f3061e7a8a74527fe22e5ecbbec2cb8 ./contracts/token/ERC20/SafeERC20.sol
a41701ff3cdbb98920cd49750549d127 ./contracts/utils/Address.sol
32a66f6f4d0584b403f26f8e5e4d17b5 ./contracts/utils/Context.sol
2d9ec4de161520693c92bd68ebadf363 ./contracts/utils/MinterRole.sol
036cb43163c154f43db99590e741e801 ./contracts/utils/Ownable.sol
aefce07cec09f52f7bab7bd48b85de1c ./contracts/utils/ReentrancyGuard.sol
118d44aa29245b854a8785a01af0c4e9 ./contracts/utils/Roles.sol
d208da96b6f6c1d204ee8248087cf2d7 ./contracts/utils/SafeMath.sol
2159dd45f55a91233c87e65092c26845 ./contracts/utils/Secondary.sol
f077df9a0e4486e069b7412d46a681e7 ./readme.txt
56f4050df5d7a420b564df7921bd88a6 ./script/app.py
c6d43f119c240e865196b7bcabfe8c93 ./script/wallet.py
1.3 Table of contents
1 Executive summary ............................................................................... 2
1.1 Liability clause ............................................................................. 2
1.2 Source code checksums .............................................................. 2
1.3 Table of contents ........................................................................ 3
2 Main contract audit .............................................................................. 4
2.1 Errors known from Ethereum ..................................................... 4
3 Clever Token overview .......................................................................... 5
3.1 Externally accessible methods .................................................... 5
3.2 State variables ............................................................................ 6
4 Clever Protocol ..................................................................................... 7
4.1 Externally accessible methods .................................................... 7
4.2 State variables ............................................................................ 8
5 TimedSwap mechanism ........................................................................ 9
5.1 Externally accessible methods .................................................... 9
5.2 State variables .......................................................................... 10
6 Cycle distribution tests ....................................................................... 11
7 Token mechanisms tests ..................................................................... 13
8 Comments and suggestions ................................................................ 24
4
2 Main contract audit
2.1 Errors known from Ethereum
✓ Reentrancy attack
The contracts adhere to ERC20 protocol and use OpenZeppelin standards where possible.
Critical methods that manipulate funds are protected with nonReentrant modifier and are
therefore safe against these types of attacks.
✓ Race conditions
Flow of the system is linear and straightforward. Nothing time-sensitive and requiring
synchronicity is performed.
✓ Integer over / underflow
Contracts use the SafeMath library, which prevents this class of errors.
✓ Timestamps
Custom logic dependent on block.timestamp is a source of many leaks as it can be
influenced by the miners. The contract is safe from any such attacks. Time is used to calculate
distribution awards upon activation and if the activation has to be performed in the first place.
Activation of each distribution cycle is protected with a time-based lock and can be performed
only once per cycle.
✓ Library dependencies
All used dependencies are in the source files. Some issues regarding following common
standards have been found, but are not critical. Rather they emerged as a quick-and-dirty
solution and could be fixed in the future version of the project.
✓ Front-running
Front running doesn’t occur here because foreseeing transactions before visible in the
blocks won’t have any bad results for the contract. This kind of attack is dangerous when a
contract acts like a market, because it if one can analyse the buy/sell transactions before they
appear in the block, he could influence the price of assets being managed.
✓ DoS
Neither of the contracts can be rendered inoperable by the users
5
✓ Insufficient gas griefing
The contracts don’t use any low level contract calls, thus this error won’t occur.
This attack may be possible on a contract which accepts generic data and uses it to make a call another contract (a 'sub-call') via the low level address.call() function, as is often the case with multisignature and transaction relayer contracts
3 Clever Token overview
3.1 Externally accessible methods
addMinter(address account) OK Standard
12approve(address spender, uint256 amount)bool OK Standard
decreaseAllowance(address spender, uint256 subtractedValue)bool
OK Standard
distribute(uint256 _cycle, uint256 _percentageFactor) OK Standard
increaseAllowance(address spender, uint256 addedValue)bool
OK Standard
mint(address account, uint256 amount)bool OK Safe; only minters like TimedSwap can mint
primitiveDistribution(uint16 _cycle, uint256 _percentage)
Vulnerability -> OK
Public function that allows altering the amount of tokens; Deleted
renounceMinter() OK Standard
renounceOwnership() OK Standard
setProtocol(address _protocol) OK Safe; only owner can access
transfer(address to, uint256 value)bool OK Standard
transferFrom(address from, address to, uint256 value)bool
OK Standard
transferOwnership(address newOwner) OK Standard
Protocol()address OK Read-only
6
allowance(address owner, address spender)uint256 OK Standard
balanceOf(address account)uint256 OK Safe
cap()uint256 OK Read-only
decimals()uint8 OK Read-only
isMinter(address account)bool OK Standard
isOwner()bool OK Standard
name()string OK Read-only
owner()address OK Read-only
symbol()string OK Read-only
totalSupply()uint256 OK Read-only
3.2 State variables
mapping (address => uint256) internal _balances OK
mapping (address => mapping (address => uint256)) internal _allowances
OK
uint256 internal _totalSupply OK
address public Protocol OK Safe; set by external method, only owner has access
uint256 private fragsPerToken OK
uint256 private lastCyclePaid OK
uint256 private DECIMALS OK
uint256 private MAX_UINT256 OK
7
uint256 private TOTAL_FRAGS OK
bool private lockedSwap OK
uint256 internal _cap OK
4 Clever Protocol
4.1 Externally accessible methods
checkIsLive()bool OK
distributeCycleAward()uint256 OK
renounceOwnership() OK Standard
setTimedSwap(address _TimedSwap) OK
timeRestrictedWithdraw() OK Works after 8th cycle
transferOwnership(address newOwner) OK Standard
cyclesCompleted()uint256 OK Read-only
fortnight()uint256 Suggestion
It's an auto-generated getter for something that should be internal to the contract
getCycle()uint256 OK Read-only
getDay()uint256 OK Read-only
8
getElapsedTime()uint256 OK Read-only
isLive()bool OK Read-only
isOwner()bool OK Standard
owner()address OK Read-only
4.2 State variables
CleverToken private token OK Safe; set only in constructor
TimedSwap private ico OK Safe; set only with setTimedSwap
uint256 private openingTime OK Safe; set only when setTimedSwap is called
uint256 private closingTime OK Safe; set only when setTimedSwap is called
uint256 public fortnight Suggestion
Should be a private constant
bool public isLive OK
uint256 default MAX_SUPPLY OK
uint default DECIMALS OK
address default admin OK
uint256 private cycles OK
9
mapping (uint256 => uint256) private cycleAwards OK
mapping (uint256 => uint256) private cycleBonus OK
mapping (uint256 => uint256) private payoutPercent OK
mapping (uint256 => bool) private cyclePaid OK
mapping (uint256 => bool) private ETHflushed OK
5 TimedSwap mechanism
5.1 Externally accessible methods
buyTokens(address beneficiary) OK Standard
closingTime()uint256 OK Read-only
getCurrentRate()uint256 OK Safe
getElapsedTime()uint256 OK Read-only
initialRate()uint256 OK Read-only
isOpen()bool OK Standard
openingTime()uint256 OK Read-only
rate()uint256 OK Read-only
token()address OK Read-only
wallet()address OK Read-only
10
weiRaised()uint256 OK Read-only
5.2 State variables
CleverToken private _token OK
address internal _protocol OK
uint256 private _rate OK
uint256 private _weiRaised OK
uint256 private _openingTime Suggestion Shouldn't be a hardcoded value
uint256 private _closingTime Suggestion Shouldn't be a hardcoded value
uint256 private _firstRate OK
uint256 private _secondRate OK
uint256 private _thirdRate OK
uint256 private _fourthRate OK
uint256 private _finalRate OK
uint256 private _firstWindow OK
uint256 private _secondWindow OK
11
uint256 private _thirdWindow OK
uint256 private _fourthWindow OK
uint256 private _finalWindow OK
6 Cycle distribution tests
The following tests were run on the project with successful execution, proving that the
distribution mechanism of the token is correct.
const { assertReverts, assertEqualBN, getTokenState, getBalances, maybeBNToString, assertEqualBNArrays, asyncForEach, range, } = require("./utils.js");
const TimedSwap = artifacts.require("TimedSwap"); const CleverTokenMock = artifacts.require("CleverTokenMock"); const CleverProtocol = artifacts.require("CleverProtocol"); const web3 = global.web3; const BN = web3.utils.BN;
const oneHour = 60 * 60; const oneDay = 24 * oneHour; const oneMonth = 30 * oneDay;
contract('CleverProtocol', async (accounts) => { const admin = accounts[0]; const anyone = accounts[7];
const tokenAddr = CleverTokenMock.address; const protocolAddr = CleverProtocol.address; var timedSwapAddr;
var token, protocol, timedSwap;
it('everything is deployed; with TimedSwap in the past', async () => { token = await CleverTokenMock.deployed(); protocol = await CleverProtocol.deployed();
const blockNumber = await web3.eth.getBlockNumber(); const timestamp = (await web3.eth.getBlock(blockNumber)).timestamp; timedSwap = await TimedSwap.new(protocolAddr, tokenAddr, timestamp - (45 *
oneDay), oneMonth, {from: admin});
12
timedSwapAddr = timedSwap.address;
await token.setProtocol.sendTransaction(protocolAddr, {from: admin}); await token.addMinter.sendTransaction(timedSwapAddr, {from: admin}); await protocol.setTimedSwap.sendTransaction(timedSwapAddr, {from: admin}); await protocol.setMockToken.sendTransaction(tokenAddr, {from: admin}); });
it('getCycle returns proper values', async () => { const fewBelow51 = [...Array(17).keys()]; const fewPotentiallyErrorProne = fewBelow51. concat(range(47,53)). concat(range(97,103)). concat(range(197, 203));
await asyncForEach(fewPotentiallyErrorProne, async cycleToGenerate => { let blockNumber = await web3.eth.getBlockNumber(); let timestamp = (await web3.eth.getBlock(blockNumber)).timestamp; let tempTimedSwap = await TimedSwap.new(protocolAddr, tokenAddr, timestamp -
(oneMonth + oneDay + (cycleToGenerate * 14 * oneDay)), oneMonth, {from: admin}); let tempTimedSwapAddr = tempTimedSwap.address;
await protocol.setTimedSwap.sendTransaction(tempTimedSwapAddr, {from: admin}); await protocol.checkIsLive({from: anyone});
let currentCycle = await protocol.getCycle.call(); assert.equal(currentCycle, cycleToGenerate);
})
// clean-up await protocol.setTimedSwap.sendTransaction(timedSwapAddr, {from: admin}); });
it('ico has closed', async () => { const openingTime = await timedSwap.openingTime.call(); const closingTime = await timedSwap.closingTime.call(); assert.ok(closingTime > openingTime);
// WARNING TimedSwap has to be added as a minter to the Token for that to work!! assert.equal(await timedSwap.hasClosed.call(), true); });
it('protocol is live; anyone can trigger', async () => { await protocol.checkIsLive({from: anyone}); assert.equal(await protocol.checkIsLive.call(), true, "Protocol is live");
const currentCycle = await protocol.getCycle.call(); assert.equal(currentCycle, 1); });
it('checking distribution', async () => { let balance = await web3.eth.getBalance(protocol.address); assert.equal(balance, 0, "Balance prior should be 0"); protocol.distributeCycleAward.sendTransaction({from: anyone});
let lastCycle = await token.lastCycleSet(); let totalPercentage = await token.totalPercentageSet(); assertEqualBNArrays([lastCycle, totalPercentage], [1, 11000], "") });
});
13
The output:
7 Token mechanisms tests
The following tests:
const { assertReverts, assertEqualBN, getTokenState, getBalances, maybeBNToString, assertEqualBNArrays, asyncForEach, } = require("./utils.js");
const CleverToken = artifacts.require("CleverToken"); const CleverProtocol = artifacts.require("CleverProtocol"); const web3 = global.web3; const BN = web3.utils.BN;
const thirtyZeroes = "0".repeat(30); const baseFPT = new BN("1" + thirtyZeroes); const baseTotalFrags = new BN("1" + thirtyZeroes + thirtyZeroes);
var protocolConfirmations = 0;
14
contract('CleverToken', async (accounts) => { const admin = accounts[0];
// we can't use the real address of the deployed contract because ganache can't
act in its name // so we change the address temporarily for something that can be used const protocol = accounts[0]; const hodler = accounts[1]; const randomBloke = accounts[7];
it('should be allowed only for the owner to make changes', async () => { const tk = await CleverToken.deployed(); const pre_confirmations = await tk.getPastEvents( 'LogProtocolSet', { fromBlock:
0, toBlock: 'latest' } ); const conf_length = pre_confirmations.length;
await tk.setProtocol(protocol, {from: admin}); const confirmations = await tk.getPastEvents( 'LogProtocolSet', { fromBlock: 0,
toBlock: 'latest' } );
assert.equal(confirmations.length, conf_length + 1);
let addError; try { //contract throws error and reverts await tk.setProtocol(randomBloke, {from: randomBloke}); } catch (error) { addError = error; } assert.notEqual(addError, undefined, 'Error must be thrown');
const unchanged_confirmations = await tk.getPastEvents( 'LogProtocolSet', {
fromBlock: 0, toBlock: 'latest' } ); assert.equal(unchanged_confirmations.length, confirmations.length);
protocolConfirmations = confirmations.length; });
it('should be allowed only for the protocol to distribute', async () => { const tk = await CleverToken.deployed();
await assertReverts(() => tk.distribute(1, 5000, {from: randomBloke}));
});
it('distributes not before cycle 1', async () => { const tk = await CleverToken.deployed();
const isLocked = await tk.isLockedSwap.call(); assert.equal(isLocked, false);
const err = await assertReverts(() => tk.distribute(0, 5000, {from: protocol})); assert.equal(err.reason, "Cycle attemptin to be paid out is in the past!");
});
it('mints', async () => { const tk = await CleverToken.deployed();
assert.equal(await tk.isMinter.call(admin), true); await tk.mint(hodler, 2137, {from: admin});
15
const [totalSupply, fpt, totalFrags] = await getTokenState(tk);
assert.equal(totalSupply, 2137); assertEqualBN(fpt, baseFPT); assertEqualBN(totalFrags, baseTotalFrags);
const trueBalance = await tk.trueBalanceOf.call(hodler); const fauxBalance = await tk.balanceOf.call(hodler); assert.equal(trueBalance.toString(), "2137000000000000000000000000000000"); assert.equal(fauxBalance.toString(), "2137"); });
it("it doesn't burn before swapping phase is over", async () => { const tk = await CleverToken.deployed(); const err = await assertReverts(() => tk.burn(2137, {from: hodler})); assert.equal(err.reason, "Swapping phase is not over!"); });
it.skip('adminBurn fails with DIVISION BY ZERO lol', async () => { const tk = await CleverToken.deployed();
// burning all available tokens held by one account results in DBZ // CAVEAT: I have removed requirement of lockedSwap for that in _burn // to prove a point I should do that _after_ distribution, but it doesn't matter
much const err = await assertReverts(() => tk.adminBurn(hodler, 2137, {from:
admin})); assert.equal(err.reason, "SafeMath: division by zero"); });
it("distributes - doubling tokens", async () => { const tk = await CleverToken.deployed();
const preState = await getTokenState(tk);
await tk.distribute(1, 100000, {from: protocol}); // + 100% assert.equal(await tk.isLockedSwap.call(), true);
const postState = await getTokenState(tk);
assert.deepStrictEqual( preState.map((x) => x.toString()), [ '2137', '1000000000000000000000000000000', '1000000000000000000000000000000000000000000000000000000000000' ] );
assert.deepStrictEqual( postState.map((x) => x.toString()), [ '4274', '500000000000000000000000000000', '2137000000000000000000000000000000' ] );
const [trueBalance, fauxBalance] = await getBalances(tk, hodler); assertEqualBN(trueBalance, "2137000000000000000000000000000000"); assertEqualBN(fauxBalance, "4274"); });
16
it("burns - burning half", async () => { const tk = await CleverToken.deployed(); await tk.burn(2137, {from: hodler}); const [trueBalance, fauxBalance] = await getBalances(tk, hodler) assertEqualBN(trueBalance, "1068500000000000000000000000000000"); assertEqualBN(fauxBalance, "2137");
const postState = await getTokenState(tk); assertEqualBNArrays( postState, [ '2137', '500000000000000000000000000000', '1068500000000000000000000000000000' ] ) });
});
contract('CleverToken verbose multiparty even', async (accounts) => { const admin = accounts[0]; const protocol = accounts[0]; const hodler_one = accounts[1]; const hodler_two = accounts[2]; const hodler_three = accounts[3]; const hodlers = [hodler_one, hodler_two, hodler_three] const randomBloke = accounts[7];
const freeTokens = 1000; const numberOfParties = hodlers.length;
it("is it at least a new instance?", async () => { const tk = await CleverToken.deployed(); assert.equal(await tk.isLockedSwap.call(), false); assertEqualBNArrays( await getTokenState(tk), [0, baseFPT, baseTotalFrags] ); });
it(`mint ${freeTokens} tokens evenly between ${numberOfParties} parties`, async ()
=> { const tk = await CleverToken.deployed(); await tk.setProtocol(protocol, {from: admin}); await tk.mint(hodler_one, freeTokens, {from: admin}); await tk.mint(hodler_two, freeTokens, {from: admin}); await tk.mint(hodler_three, freeTokens, {from: admin});
const balanceEach = [freeTokens + thirtyZeroes, freeTokens]
await asyncForEach(hodlers, async hodler => { assertEqualBNArrays( await getBalances(tk, hodler), balanceEach, "True number reflects numbers of fragments, faux of the tokens." ); }); });
it("distribute & double the tokens in rewards", async () => { const tk = await CleverToken.deployed();
17
const preState = await getTokenState(tk); assertEqualBNArrays( preState, [ freeTokens * numberOfParties, baseFPT, baseTotalFrags ], "Base values prior to distribution are mostly meaningless." );
await tk.distribute(1, 100000, {from: protocol}); // + 100% assert.equal(await tk.isLockedSwap.call(), true);
const postState = await getTokenState(tk);
// this might look stupid, but JS will turn this into a number with exp base const halfOfBaseFPT = new BN("5" + "0".repeat(29)); assertEqualBNArrays( postState, [ freeTokens * numberOfParties * 2, halfOfBaseFPT, (freeTokens * numberOfParties) + thirtyZeroes, ], "Total supply increased twice, fragmentsPerToken got halved, amount of
fragments got recalculated." );
const balanceEach = [freeTokens + thirtyZeroes, 2 * freeTokens] await asyncForEach(hodlers, async hodler => { assertEqualBNArrays( await getBalances(tk, hodler), balanceEach, "Only faux balance gets doubled for each hodler." ); }); });
it("globalBurn half", async () => { const tk = await CleverToken.deployed();
const preBalanceEach = [freeTokens + thirtyZeroes, 2 * freeTokens] await asyncForEach(hodlers, async hodler => { assertEqualBNArrays( await getBalances(tk, hodler), preBalanceEach, "We start with double the faux balance each." ); });
await tk.globalBurn(50000, {from: admin}); // burn half
const postState = await getTokenState(tk); assertEqualBNArrays( postState, [ freeTokens * numberOfParties, baseFPT, (freeTokens * numberOfParties) + thirtyZeroes, ], "Total fragments got halved. " + "We return to the state after first distribution." );
18
const postBalanceEach = [new BN(preBalanceEach[0]), new BN(preBalanceEach[1]) /
2] await asyncForEach(hodlers, async hodler => { assertEqualBNArrays( await getBalances(tk, hodler), postBalanceEach, "Faux balance got halved for each hodler." ); });
});
it("burn half of the tokens for each hodler in sequence", async () => { const tk = await CleverToken.deployed();
const preState = await getTokenState(tk);
const balanceBeforeBurn = await getBalances(tk, hodler_one);
// we should take the "faux" value as this is the number of tokens const halfOfTokens = balanceBeforeBurn[1] / 2;
const balanceAfterBurn = [new BN(balanceBeforeBurn[0]) / 2, new
BN(balanceBeforeBurn[1]) / 2] await asyncForEach(hodlers, async hodler => { await tk.burn((halfOfTokens), {from: hodler}); assertEqualBNArrays( await getBalances(tk, hodler), balanceAfterBurn, "True and faux balance got halved for each hodler." ); });
const predictedPostState = [ new BN(preState[0]) / 2, preState[1], new BN(preState[2]) / 2, ]
const postState = await getTokenState(tk); assertEqualBNArrays( postState, predictedPostState, "Total supply and total fragments got halved." + preState.map(maybeBNToString) ); });
});
contract('CleverToken verbose multiparty uneven', async (accounts) => { const admin = accounts[0]; const protocol = accounts[0]; const hodler_one = accounts[1]; const hodler_two = accounts[2]; const hodler_three = accounts[3]; const hodlers = [hodler_one, hodler_two, hodler_three]; const randomBloke = accounts[7];
const tokensEach = [1000, 10000, 100000]; const numberOfParties = hodlers.length; const sumOfTokens = tokensEach.reduce((accum, tok) => accum + tok, 0);
it(`mint ${tokensEach} tokens between ${numberOfParties} parties`, async () => {
19
const tk = await CleverToken.deployed(); await tk.setProtocol(protocol, {from: admin});
await asyncForEach(hodlers, tokensEach, async (hodler, tokens) => { await tk.mint(hodler, tokens, {from: admin}); });
await asyncForEach(hodlers, tokensEach, async (hodler, tokens) => { assertEqualBNArrays( await getBalances(tk, hodler), [tokens + thirtyZeroes, tokens] ); }); });
it("distribute & double the tokens in rewards", async () => { const tk = await CleverToken.deployed();
const preState = await getTokenState(tk); assertEqualBNArrays( preState, [ sumOfTokens, baseFPT, baseTotalFrags ] ); await tk.distribute(1, 100000, {from: protocol}); // + 100% assert.equal(await tk.isLockedSwap.call(), true);
const postState = await getTokenState(tk);
const halfOfBaseFPT = new BN(preState[1]) / 2; assertEqualBNArrays( postState, [ sumOfTokens * 2, halfOfBaseFPT, sumOfTokens + thirtyZeroes ], "Total supply increased twice, fragmentsPerToken got halved, amount of
fragments got recalculated." );
});
});
async function globalBurn(hodlers, admin, percentage) { const tk = await CleverToken.deployed();
const percent1e5 = percentage * 1000;
await tk.globalBurn(percent1e5, {from: admin}); // burn half }
async function localBurnForEach(hodlers, admin, percentage) { const tk = await CleverToken.deployed();
await asyncForEach(hodlers, async hodler => { const balance = await tk.balanceOf(hodler); const toBurn = (balance * percentage) / 100; await tk.burn(toBurn, {from: hodler}); });
20
}
[ // balances, bonus %, intermediate actions [(fn_name, args), ...], balancesAfter,
?tokenState [[1000, 1000, 1000 ], 100, [], [2000, 2000, 2000]], [[1000, 1000, 1000 ], 50, [], [1500, 1500, 1500]], [[1000, 1000, 1000 ], 30, [], [1300, 1300, 1300]],
[[1000, 1000, 1000 ], 100, [[globalBurn, [20]]], [1600, 1600,
1600]], [[1000, 1000, 1000 ], 100, [[localBurnForEach, [20]]], [1600, 1600,
1600]],
// now we are testing the burns // last array is [totalSupply, fragPerToken,
totalFrags] [[1000, 1000, 1000 ], 100, [[globalBurn, [50]]], [1000, 1000, 1000], [3000,'1000000000000000000000000000000','3000000000000000000000000000000000']], [[1000, 1000, 1000 ], 100, [[localBurnForEach, [50]]], [1000, 1000, 1000], [3000,'500000000000000000000000000000','1500000000000000000000000000000000']],
[[1000, 1000, 1000 ], 20, [[globalBurn, [20]]], [960, 960, 960], [2880,'1041666666666666700000000000000','3000000000000000000000000000000000']], [[1000, 1000, 1000 ], 20, [[localBurnForEach, [20]]], [960, 960, 960], [2880,'833333333333333400000000000000','2400000000000000000000000000000000']],
].forEach(([tokensEach, rewardPercentage, actions, afterTokensEach,
expectedPostState],ind) => { contract(`CleverToken multiparty scenario ${ind}`, async (accounts) => { const admin = accounts[0]; const protocol = accounts[0]; // terrible hack const hodler_one = accounts[1]; const hodler_two = accounts[2]; const hodler_three = accounts[3]; const hodlers = [hodler_one, hodler_two, hodler_three]; const numberOfParties = hodlers.length; const sumOfTokens = tokensEach.reduce((accum, tok) => accum + tok, 0);
const percent1e5 = rewardPercentage * 1000; // we shouldn't use floats, but for round numbers and sensible fractions JS
behaves relatively sane const rewardMultiplier = 1 + (rewardPercentage / 100);
const actionContext = [hodlers, admin];
it("it's a fresh instance", async () => { const tk = await CleverToken.deployed(); assert.equal(await tk.isLockedSwap.call(), false); assertEqualBNArrays( await getTokenState(tk), [0, baseFPT, baseTotalFrags], "Sanity check." ); });
it(`mint ${tokensEach} tokens between ${numberOfParties} parties`, async () => { const tk = await CleverToken.deployed(); await tk.setProtocol(protocol, {from: admin});
await asyncForEach(hodlers, tokensEach, async (hodler, tokens) => { await tk.mint(hodler, tokens, {from: admin}); });
21
await asyncForEach(hodlers, tokensEach, async (hodler, tokens) => { assertEqualBNArrays( await getBalances(tk, hodler), [tokens + thirtyZeroes, tokens], "Everyone gets their share of tokens." ); }); });
it(`distribute & reward with ${rewardPercentage}%`, async () => { const tk = await CleverToken.deployed();
const preState = await getTokenState(tk); assertEqualBNArrays( preState, [ sumOfTokens, baseFPT, baseTotalFrags ] ); await tk.distribute(1, percent1e5, {from: protocol}); assert.equal(await tk.isLockedSwap.call(), true);
const postState = await getTokenState(tk);
const newFPT = new BN(preState[1]) / rewardMultiplier; assertEqualBNArrays( postState, [ sumOfTokens * rewardMultiplier, newFPT, sumOfTokens + thirtyZeroes ], "Total supply increased, fragmentsPerToken decreased, amount of fragments
got recalculated." ); });
actions.forEach(([fn, args], ind) => { it(`${ind}: ${fn.name}`, async () => { await fn(...actionContext, ...args); }); });
it("expect balances for each hodler", async () => { const tk = await CleverToken.deployed();
await asyncForEach(hodlers, afterTokensEach, async (hodler, expectedTokens) =>
{ const observedTokens = await tk.balanceOf(hodler); assert.equal( observedTokens, expectedTokens, "We are comparing what the token reports that users have." + "Observed: " + maybeBNToString(observedTokens) ); }); });
if (expectedPostState) { it("expect token state to match", async () => { const tk = await CleverToken.deployed();
const postState = await getTokenState(tk);
22
assertEqualBNArrays( postState, expectedPostState, "Comparing internal states of the token." + postState.map(maybeBNToString) + expectedPostState.map(maybeBNToString) ); }); } }); });
Result with the output:
23
24
8 Comments and suggestions
• Overwriting OpenZeppelin contracts instead of regular inheritance methods is a trust
issue seen from the get-go. While proper following of these models is not necessary for
being considered an ERC20 Token, this makes it very easy to hide malicious or error-
prone actions. Nothing of this sort has been found, when compared with the original
code taken from OZ repository, but if one decides on using said contracts as a backbone,
then the finished token should work with a drop-in replacement of imports from local
(`import ./ERC20.sol;`) to let’s say github imports on Remix (`import
"http://github.com/OpenZeppelin/openzeppelin-
solidity/contracts/token/ERC20/ERC20.sol";`). This makes it much easier to verify its
correctness “at a glance”. It is advisable to create contract implementations, that differ
in name from those already existing in Zeppelin’s repertoire.
• Arithmetics during distribution are troublesome to follow and verify, because of many
redundant computations performed. Automated checks in this case will be almost
identical in nature to writing manual tests.
• _mintAfterSwap introduces inflation that’s hard to calculate for already existing tokens,
but this might be predicted by the Designers of the Token.
• Naming conventions are misleading. For example, a variable that’s considered constant
by the Creators, TOTAL_FRAGS, is not constant at all, it is being modified during minting
after “lockedSwap” is in effect. Said again: that’s not an error, just makes the job for
future developers much more difficult.
25
Thank you!
Contact us at:
www.blockhunters.io