From b6e2116a1d62c67b03d502f969939976ecd31eaa Mon Sep 17 00:00:00 2001 From: nolash Date: Fri, 5 Feb 2021 18:52:01 +0100 Subject: [PATCH] Disambiguate fraction bit when 0 participants edge case --- python/tests/test_basic.py | 2 + python/tests/test_period.py | 78 +++++++++ python/tests/test_pure.py | 90 ++++++++++ python/tests/test_redistribution.py | 205 +++++++++++++++++++++++ solidity/RedistributedDemurrageToken.sol | 44 ++++- 5 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 python/tests/test_period.py create mode 100644 python/tests/test_pure.py create mode 100644 python/tests/test_redistribution.py diff --git a/python/tests/test_basic.py b/python/tests/test_basic.py index 01ea18c..90a711c 100644 --- a/python/tests/test_basic.py +++ b/python/tests/test_basic.py @@ -129,6 +129,8 @@ class Test(unittest.TestCase): def test_apply_tax(self): + self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact() + self.eth_tester.mine_blocks(PERIOD) tx_hash = self.contract.functions.applyTax().transact() r = self.w3.eth.getTransactionReceipt(tx_hash) diff --git a/python/tests/test_period.py b/python/tests/test_period.py new file mode 100644 index 0000000..7b83ff3 --- /dev/null +++ b/python/tests/test_period.py @@ -0,0 +1,78 @@ +# standard imports +import os +import unittest +import json +import logging + +# third-party imports +import web3 +import eth_tester +import eth_abi + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +logging.getLogger('web3').setLevel(logging.WARNING) +logging.getLogger('eth.vm').setLevel(logging.WARNING) + +testdir = os.path.dirname(__file__) + +#BLOCKTIME = 5 # seconds +TAX_LEVEL = 10000 * 2 # 2% +#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month +PERIOD = 2 + + +class Test(unittest.TestCase): + + contract = None + + def setUp(self): + eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({ + 'gas_limit': 9000000, + }) + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r') + self.bytecode = f.read() + f.close() + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r') + self.abi = json.load(f) + f.close() + + backend = eth_tester.PyEVMBackend(eth_params) + self.eth_tester = eth_tester.EthereumTester(backend) + provider = web3.Web3.EthereumTesterProvider(self.eth_tester) + self.w3 = web3.Web3(provider) + self.sink_address = self.w3.eth.accounts[9] + + c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode) + tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL, PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]}) + + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress) + + self.start_block = self.w3.eth.blockNumber + + def tearDown(self): + pass + + + def test_period(self): + tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.assertEqual(r.status, 1) + + self.eth_tester.mine_blocks(PERIOD * 10) + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('r {}'.format(r)); + self.assertEqual(r.status, 1) + + period = self.contract.functions.accountPeriod(self.w3.eth.accounts[1]).call() + self.assertEqual(period, 12) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/tests/test_pure.py b/python/tests/test_pure.py new file mode 100644 index 0000000..86ae074 --- /dev/null +++ b/python/tests/test_pure.py @@ -0,0 +1,90 @@ +# standard imports +import os +import unittest +import json +import logging +import math + +# third-party imports +import web3 +import eth_tester +import eth_abi + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +logging.getLogger('web3').setLevel(logging.WARNING) +logging.getLogger('eth.vm').setLevel(logging.WARNING) + +testdir = os.path.dirname(__file__) + +#BLOCKTIME = 5 # seconds +TAX_LEVEL = 10000 * 2 # 2% +#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month +PERIOD = 10 + + +class Test(unittest.TestCase): + + contract = None + + def setUp(self): + eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({ + 'gas_limit': 9000000, + }) + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r') + self.bytecode = f.read() + f.close() + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r') + self.abi = json.load(f) + f.close() + + + backend = eth_tester.PyEVMBackend(eth_params) + self.eth_tester = eth_tester.EthereumTester(backend) + provider = web3.Web3.EthereumTesterProvider(self.eth_tester) + self.w3 = web3.Web3(provider) + self.sink_address = self.w3.eth.accounts[9] + + c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode) + tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL, PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]}) + + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress) + + self.start_block = self.w3.eth.blockNumber + + def tearDown(self): + pass + + + def test_fractional_state(self): + with self.assertRaises(eth_tester.exceptions.TransactionFailed): + self.contract.functions.remainder(2, 1).call(); + + with self.assertRaises(eth_tester.exceptions.TransactionFailed): + remainder = self.contract.functions.remainder(0, 100001).call(); + + remainder = self.contract.functions.remainder(1, 2).call(); + self.assertEqual(remainder, 0); + + whole = 5000001 + parts = 20000 + expect = whole - (math.floor(whole/parts) * parts) + remainder = self.contract.functions.remainder(parts, whole).call(); + self.assertEqual(remainder, expect) + + parts = 30000 + expect = whole - (math.floor(whole/parts) * parts) + remainder = self.contract.functions.remainder(parts, whole).call(); + self.assertEqual(remainder, expect) + + parts = 40001 + expect = whole - (math.floor(whole/parts) * parts) + remainder = self.contract.functions.remainder(parts, whole).call(); + self.assertEqual(remainder, expect) + +if __name__ == '__main__': + unittest.main() diff --git a/python/tests/test_redistribution.py b/python/tests/test_redistribution.py new file mode 100644 index 0000000..16af5fe --- /dev/null +++ b/python/tests/test_redistribution.py @@ -0,0 +1,205 @@ +# standard imports +import os +import unittest +import json +import logging + +# third-party imports +import web3 +import eth_tester +import eth_abi + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +logging.getLogger('web3').setLevel(logging.WARNING) +logging.getLogger('eth.vm').setLevel(logging.WARNING) + +testdir = os.path.dirname(__file__) + +#BLOCKTIME = 5 # seconds +TAX_LEVEL = 10000 * 2 # 2% +#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month +PERIOD = 10 + + +class Test(unittest.TestCase): + + contract = None + + def setUp(self): + eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({ + 'gas_limit': 9000000, + }) + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r') + self.bytecode = f.read() + f.close() + + f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r') + self.abi = json.load(f) + f.close() + + backend = eth_tester.PyEVMBackend(eth_params) + self.eth_tester = eth_tester.EthereumTester(backend) + provider = web3.Web3.EthereumTesterProvider(self.eth_tester) + self.w3 = web3.Web3(provider) + self.sink_address = self.w3.eth.accounts[9] + + c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode) + tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL, PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]}) + + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress) + + self.start_block = self.w3.eth.blockNumber + logg.debug('starting at block number {}'.format(self.start_block)) + + def tearDown(self): + pass + + + def debug_periods(self): + pactual = self.contract.functions.actualPeriod().call() + pstart = self.contract.functions.periodStart().call() + pduration = self.contract.functions.periodDuration().call() + blocknumber = self.w3.eth.blockNumber; + logg.debug('actual {} start {} duration {} blocknumber {}'.format(pactual, pstart, pduration, blocknumber)) + + + # TODO: check receipt log outputs + @unittest.skip('foo') + def test_redistribution_storage(self): + self.contract.functions.mintTo(self.w3.eth.accounts[1], 2000).transact() + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('tx before {}'.format(r)) + self.assertEqual(r.status, 1) + + self.eth_tester.mine_blocks(PERIOD) + + redistribution = self.contract.functions.redistributions(0).call(); + self.assertEqual(redistribution.hex(), '000000000100000000000000000000000000000000000007d000000000000001') + + tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[0], 1000000).transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.assertEqual(r.status, 1) + + redistribution = self.contract.functions.redistributions(1).call() + self.assertEqual(redistribution.hex(), '000000000000000000000000000000000000000000000f4a1000000000000002') + + + def test_redistribution_balance_on_zero_participants(self): + supply = 1000000000000 + tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], supply).transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + + self.eth_tester.mine_blocks(PERIOD) + + tx_hash = self.contract.functions.applyTax().transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('r {}'.format(r)) + self.assertEqual(r.status, 1) + + redistribution = self.contract.functions.redistributions(0).call(); + supply = self.contract.functions.totalSupply().call() + + sink_increment = int(supply * (TAX_LEVEL / 1000000)) + for l in r['logs']: + if l.topics[0].hex() == '0x337db9c77a0769d770641c73e3282be23b15e2bddd830c219461dec832313389': # event Taxed(uint256,uint256) + period = int.from_bytes(l.topics[1], 'big') + self.assertEqual(period, 1) + b = bytes.fromhex(l.data[2:]) + remainder = int.from_bytes(b, 'big') + self.assertEqual(remainder, sink_increment) + logg.debug('period {} remainder {}'.format(period, remainder)) + + sink_balance = self.contract.functions.balanceOf(self.sink_address).call() + logg.debug('{} {}'.format(sink_increment, sink_balance)) + self.assertEqual(sink_balance, int(sink_increment * 0.98)) + self.assertEqual(sink_balance, int(sink_increment * (1000000 - TAX_LEVEL) / 1000000)) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + self.assertEqual(balance, supply - sink_increment) + + + @unittest.skip('foo') + def test_redistribution_balance_to_single(self): + z = 0 + for i in range(3): + redistribution = self.contract.functions.redistributions(0).call(); + logg.debug('foo {}'.format(redistribution.hex())) + self.debug_periods(); + + self.contract.functions.mintTo(self.w3.eth.accounts[i], 1000000*(i+1)).transact() + z += 1000000*(i+1) + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[3], 1000000).transact({'from': self.w3.eth.accounts[0]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('tx before r {}'.format(r)) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[0]).call() + self.assertEqual(balance, 0) + + self.eth_tester.mine_blocks(PERIOD) + + redistribution = self.contract.functions.redistributions(0).call(); + self.assertEqual(redistribution.hex(), '000000000100000000000000000000000000000000005b8d8000000000000001') + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[3], 0).transact({'from': self.w3.eth.accounts[0]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('tx after r {}'.format(r)) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[0]).call() + self.assertEqual(balance, int(z * 0.02 * 0.98)) + + redistribution = self.contract.functions.redistributions(1).call(); + self.assertEqual(redistribution.hex(), '000000000000000000000000000000000000000000005b8d8000000000000002') + + + #@unittest.expectedFailure + @unittest.skip('foo') + def test_redistribution_balance_to_two(self): + z = 0 + for i in range(5): + redistribution = self.contract.functions.redistributions(0).call(); + logg.debug('foo {}'.format(redistribution.hex())) + self.debug_periods(); + + self.contract.functions.mintTo(self.w3.eth.accounts[i], 1000000*(i+1)).transact() + z += 1000000*(i+1) + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[5], 1000000).transact({'from': self.w3.eth.accounts[0]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[6], 2000000).transact({'from': self.w3.eth.accounts[1]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[0]).call() + self.assertEqual(balance, 0) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + self.assertEqual(balance, 0) + + self.eth_tester.mine_blocks(PERIOD) + self.debug_periods(); + + redistribution = self.contract.functions.redistributions(0).call(); + self.assertEqual(redistribution.hex(), '00000000020000000000000000000000000000000000e4e1c000000000000001') + + tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[3], 0).transact({'from': self.w3.eth.accounts[0]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[0]).call() + #self.assertEqual(balance, 117600) + self.assertEqual(balance, int((z * 0.02 * 0.98) / 2)) + + redistribution = self.contract.functions.redistributions(1).call(); + self.assertEqual(redistribution.hex(), '00000000000000000000000000000000000000000000e4e1c000000000000002') + + +if __name__ == '__main__': + unittest.main() + + diff --git a/solidity/RedistributedDemurrageToken.sol b/solidity/RedistributedDemurrageToken.sol index ed2874c..0552edb 100644 --- a/solidity/RedistributedDemurrageToken.sol +++ b/solidity/RedistributedDemurrageToken.sol @@ -6,9 +6,9 @@ pragma solidity > 0.6.11; contract RedistributedDemurrageToken { address public owner; - uint256 public decimals; string public name; string public symbol; + uint256 public decimals; uint256 public totalSupply; uint256 public periodStart; @@ -198,7 +198,7 @@ contract RedistributedDemurrageToken { uint256 truncatedResult; if (_numParts == 0) { // no division by zero please - return _sumWhole; + revert('ERR_NUMPARTS_ZERO'); } require(_numParts < _sumWhole); // At least you are never LESS than the sum of your parts. Think about that. @@ -207,18 +207,40 @@ contract RedistributedDemurrageToken { return _sumWhole - truncatedResult; } + function applyDefaultRedistribution(bytes32 _redistribution) private returns (uint256) { + uint256 redistributionSupply; + uint256 redistributionPeriod; + uint256 unit; + uint256 truncatedResult; + + redistributionSupply = toRedistributionSupply(_redistribution); + + unit = (redistributionSupply * taxLevel) / 1000000; + //truncatedResult = (unit * taxLevel) / 1000000; + truncatedResult = (unit * 1000000) / taxLevel; + + if (truncatedResult < redistributionSupply) { + redistributionPeriod = toRedistributionPeriod(_redistribution); // since we reuse period here, can possibly be optimized by passing period instead + redistributions[redistributionPeriod-1] |= 0x8000000000000000000000000000000000000000000000000000000000000000; + } + + increaseBaseBalance(sinkAddress, unit); //truncatedResult); + return unit; + } + // sets the remainder bit for the given period and books the remainder to the sink address balance // returns false if no change was made function applyRemainderOnPeriod(uint256 _remainder, uint256 _period) private returns (bool) { - bytes32 redistribution; - - redistribution = redistributions[_period-1]; + uint256 periodSupply; if (_remainder == 0) { return false; } - redistribution |= 0x8000000000000000000000000000000000000000000000000000000000000000; - increaseBaseBalance(sinkAddress, _remainder); + + redistributions[_period-1] |= 0x8000000000000000000000000000000000000000000000000000000000000000; + + periodSupply = toRedistributionSupply(redistributions[_period-1]); + increaseBaseBalance(sinkAddress, periodSupply - _remainder); return true; } @@ -241,8 +263,12 @@ contract RedistributedDemurrageToken { redistributions.push(nextRedistribution); currentParticipants = toRedistributionParticipants(currentRedistribution); - currentRemainder = remainder(currentParticipants, totalSupply); // we can use totalSupply directly because it will always be the same as the recorded supply on the current redistribution - applyRemainderOnPeriod(currentRemainder, currentPeriod); + if (currentParticipants == 0) { + currentRemainder = applyDefaultRedistribution(currentRedistribution); + } else { + currentRemainder = remainder(currentParticipants, totalSupply); // we can use totalSupply directly because it will always be the same as the recorded supply on the current redistribution + applyRemainderOnPeriod(currentRemainder, currentPeriod); + } emit Taxed(currentPeriod, currentRemainder); return demurrageModifier; }