erc20-demurrage-token

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

commit 9de5e52c2fc228b7e27d74b548c1031c08e83b2d
parent 8af12b33c02fd15d62fa75b8cf30cd8d98366169
Author: Louis Holbrook <accounts-gitlab@holbrook.no>
Date:   Sat,  6 Feb 2021 19:01:34 +0000

Merge branch 'lash/cache-demurrage-modifier' into 'master'

Cache demurrage modifier in redistribution data

See merge request grassrootseconomics/sarafu-token!7
Diffstat:
Apython/tests/bench.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpython/tests/test_redistribution.py | 4++--
Msolidity/RedistributedDemurrageToken.sol | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 188 insertions(+), 46 deletions(-)

diff --git a/python/tests/bench.py b/python/tests/bench.py @@ -0,0 +1,72 @@ +# 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__) + +TAX_LEVEL = 10000 * 2 # 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] + + def tearDown(self): + pass + + + def test_gas_changeperiod(self): + period = 43200 + for i in range(5): + 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) + contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress) + + start_block = self.w3.eth.blockNumber + b = self.w3.eth.getBlock(start_block) + start_time = b['timestamp'] + + period_seconds = period * 60 + self.eth_tester.time_travel(start_time + period_seconds + (60 * (10 ** i))) + tx_hash = contract.functions.changePeriod().transact() + r = self.w3.eth.getTransactionReceipt(tx_hash) + + print('{} ({}): {}'.format(i, 60 * (10 ** i), r['gasUsed'])) + +if __name__ == '__main__': + unittest.main() + + diff --git a/python/tests/test_redistribution.py b/python/tests/test_redistribution.py @@ -84,14 +84,14 @@ class Test(unittest.TestCase): self.eth_tester.time_travel(self.start_time + 61) redistribution = self.contract.functions.redistributions(0).call(); - self.assertEqual(redistribution.hex(), '000000000100000000000000000000000000000000001e848000000000000001') + self.assertEqual(redistribution.hex(), '000000000000000000000000f42400000000010000000000001e848000000001') 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') + self.assertEqual(redistribution.hex(), '000000000000000000000000ef4200000000000000000000002dc6c000000002') def test_redistribution_balance_on_zero_participants(self): diff --git a/solidity/RedistributedDemurrageToken.sol b/solidity/RedistributedDemurrageToken.sol @@ -18,8 +18,10 @@ contract RedistributedDemurrageToken { 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) + //bytes32[] public redistributions; // uint1(isFractional) | uint1(unused) | uint38(participants) | uint160(value) | uint56(period) + bytes32[] public redistributions; // uint1(isFractional) | uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period) + //mapping (address => bytes32) account; // uint20(unused) | uint56(period) | uint160(value) + mapping (address => bytes32) account; // uint152(unused) | uint32(period) | uint72(value) mapping (address => bool) minter; mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage) @@ -28,7 +30,7 @@ contract RedistributedDemurrageToken { event Transfer(address indexed _from, address indexed _to, uint256 _value); 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 Debug(bytes32 _foo); event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount); event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value); @@ -44,7 +46,7 @@ contract RedistributedDemurrageToken { demurrageModifier |= (1 << 128); taxLevel = _taxLevelMinute; // 38 decimal places sinkAddress = _defaultSinkAddress; - bytes32 initialRedistribution = toRedistribution(0, 0, 1); + bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1); redistributions.push(initialRedistribution); minimumParticipantSpend = 10 ** uint256(_decimals); } @@ -70,20 +72,24 @@ contract RedistributedDemurrageToken { periodCount = actualPeriod() - toDemurragePeriod(demurrageModifier); - currentDemurrageAmount = toTaxPeriodAmount(anchorDemurrageAmount, periodCount); + currentDemurrageAmount = decayBy(anchorDemurrageAmount, periodCount); return (baseBalance * currentDemurrageAmount) / (ppmDivider * 1000000); } /// Balance unmodified by demurrage function getBaseBalance(address _account) private view returns (uint256) { - return uint256(account[_account]) & 0x00ffffffffffffffffffffffffffffffffffffffff; + //return uint256(account[_account]) & 0x00ffffffffffffffffffffffffffffffffffffffff; + return uint256(account[_account]) & 0xffffffffffffffffff; } /// Increases base balance for a single account function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) { uint256 oldBalance; uint256 newBalance; + uint256 workAccount; + + workAccount = uint256(account[_account]); // | (newBalance & 0xffffffffffffffffff); if (_delta == 0) { return false; @@ -92,8 +98,12 @@ contract RedistributedDemurrageToken { oldBalance = getBaseBalance(_account); newBalance = oldBalance + _delta; require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value - account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000); - account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff); + //account[_account] &= bytes32(0xfffffffffffffffffffffff0000000000000000000000000000000000000000); + //account[_account] = bytes32(uint256(account[_account]) & 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000); + workAccount &= 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000; + //account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff); + workAccount |= newBalance & 0xffffffffffffffffff; + account[_account] = bytes32(workAccount); return true; } @@ -101,6 +111,9 @@ contract RedistributedDemurrageToken { function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) { uint256 oldBalance; uint256 newBalance; + uint256 workAccount; + + workAccount = uint256(account[_account]); // | (newBalance & 0xffffffffffffffffff); if (_delta == 0) { return false; @@ -109,8 +122,11 @@ contract RedistributedDemurrageToken { oldBalance = getBaseBalance(_account); require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard newBalance = oldBalance - _delta; - account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000); - account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff); + //account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000); + workAccount &= 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000; + //account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff); + workAccount |= newBalance & 0xffffffffffffffffff; + account[_account] = bytes32(workAccount); return true; } @@ -121,7 +137,6 @@ contract RedistributedDemurrageToken { require(minter[msg.sender]); - applyDemurrage(); changePeriod(); baseAmount = _amount; totalSupply += _amount; @@ -132,28 +147,35 @@ contract RedistributedDemurrageToken { } // Deserializes the redistribution word - function toRedistribution(uint256 _participants, uint256 _value, uint256 _period) private pure returns(bytes32) { + // uint1(isFractional) | uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period) + function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) { bytes32 redistribution; - redistribution |= bytes32((_participants & 0x7fffffffff) << 216); - redistribution |= bytes32((_value & 0xffffffffffffffffffffffff) << 56); - redistribution |= bytes32(_period & 0xffffffffffffff); + redistribution |= bytes32((_demurrageModifierPpm & 0x0fffff) << 140); + redistribution |= bytes32((_participants & 0x0fffffffff) << 104); + redistribution |= bytes32((_value & 0xffffffffffffffffff) << 32); + redistribution |= bytes32(_period & 0xffffffff); return redistribution; } // Serializes the demurrage period part of the redistribution word function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) { - return uint256(redistribution & 0x00000000000000000000000000000000000000000000000000ffffffffffffff); + return uint256(redistribution) & 0xffffffff; } // Serializes the supply part of the redistribution word function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) { - return uint256(redistribution & 0x0000000000ffffffffffffffffffffffffffffffffffffffff00000000000000) >> 56; + return uint256(redistribution & 0x00000000000000000000000000000000000000ffffffffffffffffff00000000) >> 32; } // Serializes the number of participants part of the redistribution word function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) { - return uint256(redistribution & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216; + return uint256(redistribution & 0x00000000000000000000000000000fffffffff00000000000000000000000000) >> 104; + } + + // Serializes the number of participants part of the redistribution word + function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) { + return uint256(redistribution & 0x000000000000000000000000fffff00000000000000000000000000000000000) >> 140; } // Client accessor to the redistributions array length @@ -163,16 +185,19 @@ contract RedistributedDemurrageToken { // Add number of participants for the current redistribution period by one function incrementRedistributionParticipants() private returns (bool) { - uint256 currentRedistribution; + bytes32 currentRedistribution; + uint256 tmpRedistribution; uint256 participants; - currentRedistribution = uint256(redistributions[redistributions.length-1]); - participants = ((currentRedistribution & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216) + 1; - currentRedistribution &= 0x8000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff; - currentRedistribution |= participants << 216; + currentRedistribution = redistributions[redistributions.length-1]; + participants = toRedistributionParticipants(currentRedistribution) + 1; + tmpRedistribution = uint256(currentRedistribution); + tmpRedistribution &= 0xfffffffffffffffffffffffffffff000000000ffffffffffffffffffffffffff; + tmpRedistribution |= (participants & 0x0fffffffff) << 104; - //emit Debug(participants); - redistributions[redistributions.length-1] = bytes32(currentRedistribution); + redistributions[redistributions.length-1] = bytes32(tmpRedistribution); + + return true; } // Save the current total supply amount to the current redistribution period @@ -180,10 +205,11 @@ contract RedistributedDemurrageToken { uint256 currentRedistribution; currentRedistribution = uint256(redistributions[redistributions.length-1]); - currentRedistribution &= 0xffffffffff0000000000000000000000000000000000000000ffffffffffffff; - currentRedistribution |= totalSupply << 56; + currentRedistribution &= 0xffffffffffffffffffffffffffffffffffffff000000000000000000ffffffff; + currentRedistribution |= totalSupply << 32; redistributions[redistributions.length-1] = bytes32(currentRedistribution); + return true; } // Get the demurrage period of the current block number @@ -206,14 +232,17 @@ contract RedistributedDemurrageToken { // Deserialize the pemurrage period for the given account is participating in function accountPeriod(address _account) public view returns (uint256) { - return (uint256(account[_account]) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) >> 160; + //return (uint256(account[_account]) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) >> 160; + return (uint256(account[_account]) & 0x00000000000000000000000000000000000000ffffffff000000000000000000) >> 72; } // Save the given demurrage period as the currently participation period for the given address function registerAccountPeriod(address _account, uint256 _period) private returns (bool) { - account[_account] &= 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff; - account[_account] |= bytes32(_period << 160); + //account[_account] &= 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff; + account[_account] &= 0xffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffff; + account[_account] |= bytes32(_period << 72); incrementRedistributionParticipants(); + return true; } // Determine whether the unit number is rounded down, rounded up or evenly divides. @@ -248,8 +277,8 @@ contract RedistributedDemurrageToken { 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; + redistributions[redistributionPeriod-1] &= 0xfffffffffffffffffffffffffffff000000000ffffffffffffffffffffffffff; // just to be safe, zero out all participant count data, in this case there will be only one + redistributions[redistributionPeriod-1] |= 0x8000000000000000000000000000000000000100000000000000000000000000; } increaseBaseBalance(sinkAddress, unit / ppmDivider); //truncatedResult); @@ -265,6 +294,7 @@ contract RedistributedDemurrageToken { return false; } + // is this needed? redistributions[_period-1] |= 0x8000000000000000000000000000000000000000000000000000000000000000; periodSupply = toRedistributionSupply(redistributions[_period-1]); @@ -294,7 +324,7 @@ contract RedistributedDemurrageToken { return false; } lastDemurrageAmount = toDemurrageAmount(demurrageModifier); - newDemurrageAmount = toTaxPeriodAmount(lastDemurrageAmount, periodCount); + newDemurrageAmount = decayBy(lastDemurrageAmount, periodCount); demurrageModifier = 0; demurrageModifier |= (newDemurrageAmount & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff); demurrageModifier |= (epochPeriodCount << 128); @@ -302,23 +332,50 @@ contract RedistributedDemurrageToken { return true; } + // Return timestamp of start of period threshold + function getPeriodTimeDelta(uint256 _periodCount) public view returns (uint256) { + return periodStart + (_periodCount * periodDuration); + } + + // Amount of demurrage cycles inbetween the current timestamp and the given target time + function demurrageCycles(uint256 _target) public view returns (uint256) { + return (block.timestamp - _target) / 60; + } + // 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 changePeriod() public returns (uint256) { + function changePeriod() public returns (bool) { bytes32 currentRedistribution; bytes32 nextRedistribution; uint256 currentPeriod; uint256 currentParticipants; uint256 currentRemainder; + uint256 currentDemurrageAmount; + uint256 nextRedistributionDemurrage; + uint256 demurrageCounts; + uint256 periodTimestamp; currentRedistribution = checkPeriod(); if (currentRedistribution == bytes32(0x00)) { - return demurrageModifier; + return false; } - //demurrageModifier -= (demurrageModifier * taxLevel) / 1000000; + currentPeriod = toRedistributionPeriod(currentRedistribution); - nextRedistribution = toRedistribution(0, totalSupply, currentPeriod + 1); + periodTimestamp = getPeriodTimeDelta(currentPeriod); + + applyDemurrage(); + currentDemurrageAmount = toDemurrageAmount(demurrageModifier); + + demurrageCounts = demurrageCycles(periodTimestamp); + if (demurrageCounts > 0) { + nextRedistributionDemurrage = growBy(currentDemurrageAmount, demurrageCounts) / ppmDivider; + } else { + nextRedistributionDemurrage = currentDemurrageAmount / ppmDivider; + } + + nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply, currentPeriod + 1); + emit Debug(bytes32(currentDemurrageAmount)); redistributions.push(nextRedistribution); currentParticipants = toRedistributionParticipants(currentRedistribution); @@ -328,11 +385,26 @@ contract RedistributedDemurrageToken { 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; + return true; + } + + function growBy(uint256 _value, uint256 _period) public view returns (uint256) { + uint256 valueFactor; + uint256 truncatedTaxLevel; + + // 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 + ((valueFactor * truncatedTaxLevel) / 1000000); + } + return (valueFactor * _value) / 1000000; } // Calculate a value reduced by demurrage by the given period - function toTaxPeriodAmount(uint256 _value, uint256 _period) public view returns (uint256) { + // TODO: higher precision + function decayBy(uint256 _value, uint256 _period) public view returns (uint256) { uint256 valueFactor; uint256 truncatedTaxLevel; @@ -368,9 +440,10 @@ contract RedistributedDemurrageToken { supply = toRedistributionSupply(periodRedistribution); baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider; - value = toTaxPeriodAmount(baseValue, period - 1); + value = decayBy(baseValue, period - 1); - account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff); + //account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff); + account[_account] &= bytes32(0xffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffff); increaseBaseBalance(_account, value); emit Redistribution(_account, period, value); @@ -386,7 +459,6 @@ contract RedistributedDemurrageToken { function approve(address _spender, uint256 _value) public returns (bool) { uint256 baseValue; - applyDemurrage(); changePeriod(); applyRedistributionOnAccount(msg.sender); @@ -401,7 +473,6 @@ contract RedistributedDemurrageToken { uint256 baseValue; bool result; - applyDemurrage(); changePeriod(); applyRedistributionOnAccount(msg.sender); @@ -418,7 +489,6 @@ contract RedistributedDemurrageToken { uint256 baseValue; bool result; - applyDemurrage(); changePeriod(); applyRedistributionOnAccount(msg.sender);