erc20-demurrage-token

ERC20 token with redistributed continual demurrage
Log | Files | Refs | README

commit cf6fd57672def9eec9e884ac0321f4b73b53a358
parent 01155f6e64bc40ed9ebc9b84f28c924e150d3a51
Author: Louis Holbrook <accounts-gitlab@holbrook.no>
Date:   Sat,  6 Feb 2021 14:18:30 +0000

Merge branch 'lash/use-timestamps' into 'master'

Use timestamps instead of blocks

See merge request grassrootseconomics/sarafu-token!4
Diffstat:
Mpython/tests/test_basic.py | 110+++++++++++++++++++++++++++++++++++++------------------------------------------
Apython/tests/test_period.py | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/tests/test_pure.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/tests/test_redistribution.py | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msolidity/RedistributedDemurrageToken.sol | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
5 files changed, 583 insertions(+), 124 deletions(-)

diff --git a/python/tests/test_basic.py b/python/tests/test_basic.py @@ -3,6 +3,7 @@ import os import unittest import json import logging +import datetime # third-party imports import web3 @@ -18,9 +19,11 @@ logging.getLogger('eth.vm').setLevel(logging.WARNING) testdir = os.path.dirname(__file__) #BLOCKTIME = 5 # seconds -TAX_LEVEL = 10000 * 2 # 2% +TAX_LEVEL = int(10000 * 2) # 2% +# calc "1-(0.98)^(1/518400)" <- 518400 = 30 days of blocks +# 0.00000003897127107225 #PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month -PERIOD = 10 +PERIOD = 1 class Test(unittest.TestCase): @@ -48,12 +51,15 @@ class Test(unittest.TestCase): 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]}) + tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), 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 + b = self.w3.eth.getBlock(self.start_block) + self.start_time = b['timestamp'] + def tearDown(self): pass @@ -61,10 +67,31 @@ class Test(unittest.TestCase): def test_hello(self): self.assertEqual(self.contract.functions.actualPeriod().call(), 1) - self.eth_tester.mine_blocks(PERIOD) + self.eth_tester.time_travel(self.start_time + 61) self.assertEqual(self.contract.functions.actualPeriod().call(), 2) + + def test_apply_demurrage(self): + modifier = 10 * (10 ** 37) + demurrage_modifier = self.contract.functions.demurrageModifier().call() + demurrage_modifier &= (1 << 128) - 1 + self.assertEqual(modifier, demurrage_modifier) + + self.eth_tester.time_travel(self.start_time + 59) + demurrage_modifier = self.contract.functions.demurrageModifier().call() + demurrage_modifier &= (1 << 128) - 1 + self.assertEqual(modifier, demurrage_modifier) + + self.eth_tester.time_travel(self.start_time + 61) + tx_hash = self.contract.functions.applyDemurrage().transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + + demurrage_modifier = self.contract.functions.demurrageModifier().call() + demurrage_modifier &= (1 << 128) - 1 + self.assertEqual(int(98 * (10 ** 36)), demurrage_modifier) + + def test_mint(self): tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact() r = self.w3.eth.getTransactionReceipt(tx_hash) @@ -80,6 +107,26 @@ class Test(unittest.TestCase): balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() self.assertEqual(balance, 2000) + self.eth_tester.time_travel(self.start_time + 61) + balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + self.assertEqual(balance, int(2000 * 0.98)) + + + def test_base_amount(self): + tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.assertEqual(r.status, 1) + + self.eth_tester.time_travel(self.start_time + 61) + + self.contract.functions.applyDemurrage().transact() + demurrage_modifier = self.contract.functions.demurrageModifier().call() + demurrage_amount = self.contract.functions.toDemurrageAmount(demurrage_modifier).call() + logg.debug('d {} {}'.format(demurrage_modifier.to_bytes(32, 'big').hex(), demurrage_amount)) + + a = self.contract.functions.toBaseAmount(1000).call(); + self.assertEqual(a, 1020) + def test_transfer(self): tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact() @@ -128,60 +175,5 @@ class Test(unittest.TestCase): self.assertEqual(balance_alice, 500) - def test_apply_tax(self): - self.eth_tester.mine_blocks(PERIOD) - tx_hash = self.contract.functions.applyTax().transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(self.contract.functions.redistributionCount().call(), 2) - self.assertEqual(self.contract.functions.demurrageModifier().call(), 980000) - - self.eth_tester.mine_blocks(PERIOD) - tx_hash = self.contract.functions.applyTax().transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(self.contract.functions.redistributionCount().call(), 3) - self.assertEqual(self.contract.functions.demurrageModifier().call(), 960400) - - - def test_tax_balance(self): - tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(r.status, 1) - - self.eth_tester.mine_blocks(PERIOD) - tx_hash = self.contract.functions.applyTax().transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(r.status, 1) - - balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() - self.assertEqual(balance, 980) - - - def test_taxed_transfer(self): - tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(r.status, 1) - - self.eth_tester.mine_blocks(PERIOD) - tx_hash = self.contract.functions.applyTax().transact() - r = self.w3.eth.getTransactionReceipt(tx_hash) - self.assertEqual(r.status, 1) - - balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() - self.assertEqual(balance_alice, 980000) - - tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500000).transact({'from': self.w3.eth.accounts[1]}) - r = self.w3.eth.getTransactionReceipt(tx_hash) - logg.debug('r {}'.format(r)) - self.assertEqual(r.status, 1) - - balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() - balance_alice_trunc = int(balance_alice/1000)*1000 - self.assertEqual(balance_alice_trunc, 480000) - - balance_bob = self.contract.functions.balanceOf(self.w3.eth.accounts[2]).call() - balance_bob_trunc = int(balance_bob/1000)*1000 - self.assertEqual(balance_bob_trunc, 500000) - - if __name__ == '__main__': unittest.main() diff --git a/python/tests/test_period.py b/python/tests/test_period.py @@ -0,0 +1,80 @@ +# 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 = 1 + + +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 * (10 ** 32), 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 + b = self.w3.eth.getBlock(self.start_block) + self.start_time = b['timestamp'] + + + 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.time_travel(self.start_time + 61) + tx_hash = self.contract.functions.changePeriod().transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + self.assertEqual(r.status, 1) + + redistribution = self.contract.functions.redistributions(1).call() + self.assertEqual(2, self.contract.functions.toRedistributionPeriod(redistribution).call()) + self.assertEqual(2, self.contract.functions.actualPeriod().call()) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/tests/test_pure.py b/python/tests/test_pure.py @@ -0,0 +1,107 @@ +# 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 = int((10000 * 2) * (10 ** 32)) # 2% +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_tax_period(self): + t = self.contract.functions.taxLevel().call() + logg.debug('taxlevel {}'.format(t)) + + a = self.contract.functions.toTaxPeriodAmount(1000000, 0).call() + self.assertEqual(a, 1000000) + + a = self.contract.functions.toTaxPeriodAmount(1000000, 1).call() + self.assertEqual(a, 980000) + + a = self.contract.functions.toTaxPeriodAmount(1000000, 2).call() + self.assertEqual(a, 960400) + + a = self.contract.functions.toTaxPeriodAmount(980000, 1).call() + self.assertEqual(a, 960400) + + + 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 @@ -0,0 +1,198 @@ +# 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 = 1 + + +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 * (10 ** 32), 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 + b = self.w3.eth.getBlock(self.start_block) + self.start_time = b['timestamp'] + + + 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 + def test_redistribution_storage(self): + self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact() + self.contract.functions.mintTo(self.w3.eth.accounts[2], 1000000).transact() + + external_address = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex()) + tx_hash = self.contract.functions.transfer(external_address, 1000000).transact({'from': self.w3.eth.accounts[2]}) + tx_hash = self.contract.functions.transfer(external_address, 999999).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.time_travel(self.start_time + 61) + + redistribution = self.contract.functions.redistributions(0).call(); + self.assertEqual(redistribution.hex(), '000000000100000000000000000000000000000000001e848000000000000001') + + 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(), '000000000000000000000000000000000000000000002dc6c000000000000002') + + + 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.time_travel(self.start_time + 61) + + tx_hash = self.contract.functions.applyDemurrage().transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('r {}'.format(r)) + self.assertEqual(r.status, 1) + tx_hash = self.contract.functions.changePeriod().transact() + rr = self.w3.eth.getTransactionReceipt(tx_hash) + self.assertEqual(rr.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() == '0xa0717e54e02bd9829db5e6e998aec0ae9de796b8d150a3cc46a92ab869697755': # event Decayed(uint256,uint256,uint256,uint256) + period = int.from_bytes(l.topics[1], 'big') + self.assertEqual(period, 2) + b = bytes.fromhex(l.data[2:]) + remainder = int.from_bytes(b, 'big') + self.assertEqual(remainder, int((1000000 - TAX_LEVEL) * (10 ** 32))) + 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) + + + def test_redistribution_two_of_ten(self): + mint_amount = 100000000 + z = 0 + for i in range(10): + self.contract.functions.mintTo(self.w3.eth.accounts[i], mint_amount).transact() + z += mint_amount + + initial_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + + spend_amount = 1000000 + external_address = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex()) + self.contract.functions.transfer(external_address, spend_amount).transact({'from': self.w3.eth.accounts[1]}) + tx_hash = self.contract.functions.transfer(external_address, spend_amount).transact({'from': self.w3.eth.accounts[2]}) + r = self.w3.eth.getTransactionReceipt(tx_hash) + # No cheating! + self.contract.functions.transfer(self.w3.eth.accounts[3], spend_amount).transact({'from': self.w3.eth.accounts[3]}) + # Cheapskate! + self.contract.functions.transfer(external_address, spend_amount-1).transact({'from': self.w3.eth.accounts[4]}) + + self.assertEqual(r.status, 1) + + self.eth_tester.time_travel(self.start_time + 61) + + self.contract.functions.applyDemurrage().transact() + self.contract.functions.changePeriod().transact() + + bummer_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[3]).call() + self.assertEqual(bummer_balance, mint_amount - (mint_amount * (TAX_LEVEL / 1000000))) + logg.debug('bal {} '.format(bummer_balance)) + + bummer_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + spender_balance = mint_amount - spend_amount + spender_decayed_balance = int(spender_balance - (spender_balance * (TAX_LEVEL / 1000000))) + self.assertEqual(bummer_balance, spender_decayed_balance) + logg.debug('bal {} '.format(bummer_balance)) + + tx_hash = self.contract.functions.applyRedistributionOnAccount(self.w3.eth.accounts[1]).transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + logg.debug('log {}'.format(r.logs)) + + self.contract.functions.applyRedistributionOnAccount(self.w3.eth.accounts[2]).transact() + + redistribution_data = self.contract.functions.redistributions(0).call() + logg.debug('redist data {}'.format(redistribution_data.hex())) + + account_period_data = self.contract.functions.accountPeriod(self.w3.eth.accounts[1]).call() + logg.debug('account period {}'.format(account_period_data)) + + actual_period = self.contract.functions.actualPeriod().call() + logg.debug('period {}'.format(actual_period)) + + redistribution = int((z / 2) * (TAX_LEVEL / 1000000)) + spender_new_base_balance = ((mint_amount - spend_amount) + redistribution) + spender_new_decayed_balance = int(spender_new_base_balance - (spender_new_base_balance * (TAX_LEVEL / 1000000))) + + spender_actual_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call() + logg.debug('rrr {} {}'.format(redistribution, spender_new_decayed_balance)) + + self.assertEqual(spender_actual_balance, spender_new_decayed_balance) + + +if __name__ == '__main__': + unittest.main() + + diff --git a/solidity/RedistributedDemurrageToken.sol b/solidity/RedistributedDemurrageToken.sol @@ -6,15 +6,17 @@ 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 minimumParticipantSpend; + uint256 constant ppmDivider = 100000000000000000000000000000000; - uint256 public periodStart; - uint256 public periodDuration; - uint256 public taxLevel; // PPM - uint256 public demurrageModifier; // PPM + uint256 public immutable periodStart; // timestamp + uint256 public immutable periodDuration; // duration in SECONDS + uint256 public immutable taxLevel; // PPM per MINUTE + uint256 public demurrageModifier; // PPM uint128(block) | uint128(ppm) bytes32[] public redistributions; // uint1(isFractional) | uint1(unused) | uint38(participants) | uint160(value) | uint56(period) mapping (address => bytes32) account; // uint20(unused) | uint56(period) | uint160(value) @@ -27,22 +29,24 @@ contract RedistributedDemurrageToken { event Approval(address indexed _owner, address indexed _spender, uint256 _value); event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value); //event Debug(uint256 _foo); - event Taxed(uint256 indexed _period, uint256 remainder); + event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount); event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value); - constructor(string memory _name, string memory _symbol, uint8 _decimals, uint32 _taxLevel, uint256 _period, address _defaultSinkAddress) public { + constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress) public { owner = msg.sender; minter[owner] = true; - periodStart = block.number; - periodDuration = _period; - taxLevel = _taxLevel; + periodStart = block.timestamp; + periodDuration = _periodMinutes * 60; name = _name; symbol = _symbol; decimals = _decimals; - demurrageModifier = 1000000; + demurrageModifier = ppmDivider * 1000000; // Emulates 38 decimal places + demurrageModifier |= (1 << 128); + taxLevel = _taxLevelMinute; // 38 decimal places sinkAddress = _defaultSinkAddress; bytes32 initialRedistribution = toRedistribution(0, 0, 1); redistributions.push(initialRedistribution); + minimumParticipantSpend = 10 ** uint256(_decimals); } // Given address will be allowed to call the mintTo() function @@ -51,13 +55,24 @@ contract RedistributedDemurrageToken { minter[_minter] = true; return true; } - + /// ERC20 function balanceOf(address _account) public view returns (uint256) { - uint256 baseBalance = getBaseBalance(_account); - uint256 inverseModifier = 1000000 - demurrageModifier; - uint256 balanceModifier = (inverseModifier * baseBalance) / 1000000; - return baseBalance - balanceModifier; + uint256 baseBalance; + uint256 anchorDemurrageAmount; + uint256 anchorDemurragePeriod; + uint256 currentDemurrageAmount; + uint256 periodCount; + + baseBalance = getBaseBalance(_account); + anchorDemurrageAmount = toDemurrageAmount(demurrageModifier); + anchorDemurragePeriod = toDemurragePeriod(demurrageModifier); + + periodCount = actualPeriod() - toDemurragePeriod(demurrageModifier); + + currentDemurrageAmount = toTaxPeriodAmount(anchorDemurrageAmount, periodCount); + + return (baseBalance * currentDemurrageAmount) / (ppmDivider * 1000000); } /// Balance unmodified by demurrage @@ -69,7 +84,11 @@ contract RedistributedDemurrageToken { function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) { uint256 oldBalance; uint256 newBalance; - + + if (_delta == 0) { + return false; + } + oldBalance = getBaseBalance(_account); newBalance = oldBalance + _delta; require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value @@ -83,6 +102,10 @@ contract RedistributedDemurrageToken { uint256 oldBalance; uint256 newBalance; + if (_delta == 0) { + return false; + } + oldBalance = getBaseBalance(_account); require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard newBalance = oldBalance - _delta; @@ -96,8 +119,8 @@ contract RedistributedDemurrageToken { function mintTo(address _beneficiary, uint256 _amount) external returns (bool) { require(minter[msg.sender]); - // TODO: get base amount for minting - applyTax(); + applyDemurrage(); + changePeriod(); totalSupply += _amount; increaseBaseBalance(_beneficiary, _amount); emit Mint(msg.sender, _beneficiary, _amount); @@ -162,7 +185,7 @@ contract RedistributedDemurrageToken { // Get the demurrage period of the current block number function actualPeriod() public view returns (uint256) { - return (block.number - periodStart) / periodDuration + 1; + return (block.timestamp - periodStart) / periodDuration + 1; } // Add an entered demurrage period to the redistribution array @@ -198,7 +221,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,24 +230,79 @@ contract RedistributedDemurrageToken { return _sumWhole - truncatedResult; } + // Called in the edge case where participant number is 0. It will override the participant count to 1. + // Returns the remainder sent to the sink address + function applyDefaultRedistribution(bytes32 _redistribution) private returns (uint256) { + uint256 redistributionSupply; + uint256 redistributionPeriod; + uint256 unit; + uint256 truncatedResult; + + redistributionSupply = toRedistributionSupply(_redistribution); + + unit = (redistributionSupply * 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] &= 0x0000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff; // just to be safe, zero out all participant count data, in this case there will be only one + redistributions[redistributionPeriod-1] |= 0x8000000001000000000000000000000000000000000000000000000000000000; + } + + increaseBaseBalance(sinkAddress, unit / ppmDivider); //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; + } + + + function toDemurrageAmount(uint256 _demurrage) public pure returns (uint256) { + return _demurrage & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff; + } + + function toDemurragePeriod(uint256 _demurrage) public pure returns (uint256) { + return (_demurrage & 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000) >> 128; + } + + function applyDemurrage() public returns (bool) { + uint256 epochPeriodCount; + uint256 periodCount; + uint256 lastDemurrageAmount; + uint256 newDemurrageAmount; + + epochPeriodCount = actualPeriod(); + //epochPeriodCount = (block.timestamp - periodStart) / periodDuration; // toDemurrageTime(demurrageModifier); + periodCount = epochPeriodCount - toDemurragePeriod(demurrageModifier); + if (periodCount == 0) { + return false; + } + lastDemurrageAmount = toDemurrageAmount(demurrageModifier); + newDemurrageAmount = toTaxPeriodAmount(lastDemurrageAmount, periodCount); + demurrageModifier = 0; + demurrageModifier |= (newDemurrageAmount & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff); + demurrageModifier |= (epochPeriodCount << 128); + emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, newDemurrageAmount); return true; } // Recalculate the demurrage modifier for the new period // After this, all REPORTED balances will have been reduced by the corresponding ratio (but the effecive totalsupply stays the same) - function applyTax() public returns (uint256) { + //function applyTax() public returns (uint256) { + function changePeriod() public returns (uint256) { bytes32 currentRedistribution; bytes32 nextRedistribution; uint256 currentPeriod; @@ -235,29 +313,33 @@ contract RedistributedDemurrageToken { if (currentRedistribution == bytes32(0x00)) { return demurrageModifier; } - demurrageModifier -= (demurrageModifier * taxLevel) / 1000000; + //demurrageModifier -= (demurrageModifier * taxLevel) / 1000000; currentPeriod = toRedistributionPeriod(currentRedistribution); nextRedistribution = toRedistribution(0, totalSupply, currentPeriod + 1); 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); - emit Taxed(currentPeriod, currentRemainder); + 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); + } return demurrageModifier; } // Calculate a value reduced by demurrage by the given period function toTaxPeriodAmount(uint256 _value, uint256 _period) public view returns (uint256) { uint256 valueFactor; + uint256 truncatedTaxLevel; - // TODO: doesn't work for solidity as floats are missing and using ints linearly increases the order of magnitude - // valueFactor = 1000000 * (((1000000-taxLevel)/1000000) ** _period); + // TODO: if can't get to work, reverse the iteration from current period. valueFactor = 1000000; + truncatedTaxLevel = taxLevel / ppmDivider; + for (uint256 i = 0; i < _period; i++) { - valueFactor = (valueFactor * taxLevel) / 1000000; + valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000); } - return (valueFactor * _value) / 1000000; } @@ -282,9 +364,8 @@ contract RedistributedDemurrageToken { } supply = toRedistributionSupply(periodRedistribution); - // TODO: Make sure value for balance increases round down, and that we can do a single allocation to a sink account with the difference. We can use the highest bit in "participants" for that. - baseValue = supply / participants; - value = toTaxPeriodAmount(baseValue, period-1); + baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider; + value = toTaxPeriodAmount(baseValue, period - 1); account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff); increaseBaseBalance(_account, value); @@ -295,7 +376,21 @@ contract RedistributedDemurrageToken { // Inflates the given amount according to the current demurrage modifier function toBaseAmount(uint256 _value) public view returns (uint256) { - return (_value * 1000000) / demurrageModifier; + return (_value * ppmDivider * 1000000) / toDemurrageAmount(demurrageModifier); + } + + // ERC20, triggers tax and/or redistribution + function approve(address _spender, uint256 _value) public returns (bool) { + uint256 baseValue; + + applyDemurrage(); + changePeriod(); + applyRedistributionOnAccount(msg.sender); + + baseValue = toBaseAmount(_value); + allowance[msg.sender][_spender] += baseValue; + emit Approval(msg.sender, _spender, _value); + return true; } // ERC20, triggers tax and/or redistribution @@ -303,7 +398,8 @@ contract RedistributedDemurrageToken { uint256 baseValue; bool result; - applyTax(); + applyDemurrage(); + changePeriod(); applyRedistributionOnAccount(msg.sender); // TODO: Prefer to truncate the result, instead it seems to round to nearest :/ @@ -313,29 +409,14 @@ contract RedistributedDemurrageToken { return result; } - // ERC20 transfer backend for transfer, transferFrom - function transferBase(address _from, address _to, uint256 _value) private returns (bool) { - uint256 period; - - if (!decreaseBaseBalance(_from, _value)) { - revert('ERR_TX_DECREASEBALANCE'); - } - if (!increaseBaseBalance(_to, _value)) { - revert('ERR_TX_INCREASEBALANCE'); - } - period = actualPeriod(); - if (_value > 0 && accountPeriod(_from) != period) { - registerAccountPeriod(_from, period); - } - return true; - } // ERC20, triggers tax and/or redistribution function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { uint256 baseValue; bool result; - applyTax(); + applyDemurrage(); + changePeriod(); applyRedistributionOnAccount(msg.sender); baseValue = toBaseAmount(_value); @@ -345,16 +426,17 @@ contract RedistributedDemurrageToken { return result; } - // ERC20, triggers tax and/or redistribution - function approve(address _spender, uint256 _value) public returns (bool) { - uint256 baseValue; + // ERC20 transfer backend for transfer, transferFrom + function transferBase(address _from, address _to, uint256 _value) private returns (bool) { + uint256 period; - applyTax(); - applyRedistributionOnAccount(msg.sender); + decreaseBaseBalance(_from, _value); + increaseBaseBalance(_to, _value); - baseValue = toBaseAmount(_value); - allowance[msg.sender][_spender] += baseValue; - emit Approval(msg.sender, _spender, _value); + period = actualPeriod(); + if (_value >= minimumParticipantSpend && accountPeriod(_from) != period && _from != _to) { + registerAccountPeriod(_from, period); + } return true; } }