16 Commits

Author SHA1 Message Date
lash
8c57aa02d6 WIP implement full history of account balance snapshot 2023-01-18 09:34:31 +00:00
lash
9d7b87be9c Finish repairing cli tools 2023-01-11 13:35:24 +00:00
lash
697d2bf227 Rehabilitate cli tools 2023-01-11 13:06:09 +00:00
lash
e49493def1 Decrease allowance on transferfrom 2023-01-07 07:00:31 +00:00
lash
4f1bd36c12 Add delta allowance, approve sets explicit value 2023-01-05 13:40:23 +00:00
lash
166302ef49 Bump contract version 2022-12-16 12:01:23 +00:00
lash
2ec72bbc55 Add comments, remove dead code, remove make install for dead contracts 2022-12-15 10:48:40 +00:00
lash
97415dbed2 Add simple demurrage check, asserts for longer period checks 2022-12-15 07:25:04 +00:00
lash
3b87961d13 WIP investigate apparent overflow problem 2022-12-13 13:26:27 +00:00
lash
a56f1c4869 WIP test burn over time 2022-12-13 13:10:33 +00:00
lash
ef294eaec2 Remove potentially boundless iteration in changePeriod in contract 2022-12-11 09:41:56 +00:00
lash
2f4f8bb7b9 Add test for arbitrary account demurrage honoring burn amount 2022-12-05 18:53:07 +00:00
lash
140dde7bc3 Add total burned method 2022-12-05 18:25:04 +00:00
lash
90fb95208f Add test for redistribution after burn 2022-12-05 18:17:58 +00:00
99a9915d11 Merge branch 'dev-0.2.0' into 'master'
Runaway redistribution

See merge request cicnet/erc20-demurrage-token!10
2022-05-30 07:53:22 +00:00
Louis Holbrook
20e3a783fd Runaway redistribution 2022-05-30 07:53:22 +00:00
25 changed files with 761 additions and 284 deletions

5
CAVEAT Normal file
View File

@@ -0,0 +1,5 @@
The contract is intended for slow rates of decay (e.g. 2% per month). Very high levels of decay (2% per minute) will lead to overflows, and will need a more flexible implementation to support it.
The contract is written with frequent usage in mind. If used for tokens with low usage freqency (e.g. several days idle), it is recommended to run a continuous process triggering the changePeriod() contract call, to reduce the amount of exponential calculation the application of demurrage will trigger.
When changing the period, the supply for the consecutive period will be taken at the time of code execution, and thus not necessarily at the time when the redistribution period threshold was crossed.

8
ROADMAP Normal file
View File

@@ -0,0 +1,8 @@
- 0.1.3
* Snapshot supply for crossed redistribution thresholds before minting new tokens.
- 0.1.4
* Implement natural logarithm
- 0.1.5
* Port changes from SingleNocap to SingleCap
- 0.2.0
* Make decay resolutions configurable, to support high levels of decay.

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.2

View File

@@ -1,3 +1,9 @@
- 0.2.0
* Add token burn function
* Fix gas leak when calculating decay on period change
* Remove all but SingleNocap contract in make install
* Make approve explicitly set value
* Add increaseAllowance and decreaseAllowance methods
- 0.1.1
* Settable demurrage steps for apply demurrage cli tool
- 0.1.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,17 +16,7 @@ import math
# external imports
import confini
from funga.eth.signer import EIP155Signer
from funga.eth.keystore.dict import DictKeystore
from chainlib.chain import ChainSpec
from chainlib.eth.nonce import (
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
)
import chainlib.eth.cli
from chainlib.eth.block import (
block_latest,
block_by_number,
@@ -35,8 +25,20 @@ from chainlib.eth.block import (
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import receipt
from chainlib.eth.constant import ZERO_ADDRESS
import chainlib.eth.cli
from hexathon import to_int as hex_to_int
import chainlib.eth.cli
from chainlib.eth.settings import process_settings
from chainlib.settings import ChainSettings
from chainlib.eth.cli.arg import (
Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chainlib.eth.cli.log import process_log
# local imports
import erc20_demurrage_token
@@ -45,32 +47,38 @@ from erc20_demurrage_token import (
DemurrageTokenSettings,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
script_dir = os.path.dirname(__file__)
data_dir = os.path.join(script_dir, '..', 'data')
config_dir = os.path.join(data_dir, 'config')
def process_config_local(config, arg, args, flags):
config.add(args.steps, '_STEPS', False)
return config
arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
arg_flags = ArgFlag()
arg = Arg(arg_flags)
flags = arg_flags.STD_WRITE | arg_flags.EXEC | arg_flags.WALLET
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
argparser.add_argument('--steps', type=int, default=0, help='Max demurrage steps to apply per round')
args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_fee_limit=DemurrageToken.gas(), base_config_dir=config_dir)
config.add(args.steps, '_STEPS', False)
logg = process_log(args, logg)
config = Config()
config = process_config(config, arg, args, flags)
config = process_config_local(config, arg, args, flags)
logg.debug('config loaded:\n{}'.format(config))
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
settings = ChainSettings()
settings = process_settings(settings, config)
logg.debug('settings loaded:\n{}'.format(settings))
def main():
chain_spec = settings.get('CHAIN_SPEC')
conn = settings.get('CONN')
o = block_latest()
r = conn.do(o)
@@ -87,9 +95,9 @@ def main():
block_start_timestamp = block_start.timestamp
block_start_datetime = datetime.datetime.fromtimestamp(block_start_timestamp)
gas_oracle = rpc.get_gas_oracle()
gas_oracle = settings.get('FEE_ORACLE')
c = DemurrageToken(chain_spec, gas_oracle=gas_oracle)
o = c.demurrage_timestamp(config.get('_EXEC_ADDRESS'))
o = c.demurrage_timestamp(settings.get('EXEC'))
r = conn.do(o)
demurrage_timestamp = None
@@ -120,17 +128,17 @@ def main():
last_tx_hash = None
for i in range(rounds):
signer = rpc.get_signer()
signer_address = rpc.get_sender_address()
signer = settings.get('SIGNER')
signer_address = settings.get('SENDER_ADDRESS')
nonce_oracle = rpc.get_nonce_oracle()
nonce_oracle = settings.get('NONCE_ORACLE')
c = DemurrageToken(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
(tx_hash_hex, o) = c.apply_demurrage(config.get('_EXEC_ADDRESS'), signer_address, limit=config.get('_STEPS'))
if config.get('_RPC_SEND'):
if settings.get('RPC_SEND'):
print(tx_hash_hex)
conn.do(o)
if config.get('_WAIT_ALL') or (i == rounds - 1 and config.get('_WAIT')):
if config.true('_WAIT_ALL') or (i == rounds - 1 and config.true('_WAIT')):
r = conn.wait(tx_hash_hex)
if r['status'] == 0:
sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you')

View File

@@ -14,21 +14,22 @@ import logging
# external imports
import confini
from funga.eth.signer import EIP155Signer
from funga.eth.keystore.dict import DictKeystore
from chainlib.chain import ChainSpec
from chainlib.eth.nonce import (
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
)
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import receipt
from chainlib.eth.constant import ZERO_ADDRESS
import chainlib.eth.cli
from chainlib.eth.settings import process_settings
from chainlib.settings import ChainSettings
from chainlib.eth.cli.arg import (
Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chainlib.eth.cli.log import process_log
# local imports
import erc20_demurrage_token
@@ -37,84 +38,75 @@ from erc20_demurrage_token import (
DemurrageTokenSettings,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
script_dir = os.path.dirname(__file__)
data_dir = os.path.join(script_dir, '..', 'data')
config_dir = os.path.join(data_dir, 'config')
def process_config_local(config, arg, args, flags):
config.add(args.token_name, 'TOKEN_NAME', False)
config.add(args.token_symbol, 'TOKEN_SYMBOL', False)
config.add(args.token_decimals, 'TOKEN_DECIMALS', False)
config.add(args.sink_address, 'TOKEN_SINK_ADDRESS', False)
config.add(args.redistribution_period, 'TOKEN_REDISTRIBUTION_PERIOD', False)
config.add(args.demurrage_level, 'TOKEN_DEMURRAGE_LEVEL', False)
config.add(0, 'TOKEN_SUPPLY_LIMIT', False)
return config
arg_flags = chainlib.eth.cli.argflag_std_write
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
arg_flags = ArgFlag()
arg = Arg(arg_flags)
flags = arg_flags.STD_WRITE | arg_flags.EXEC | arg_flags.WALLET
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
argparser.add_argument('--name', dest='token_name', type=str, help='Token name')
argparser.add_argument('--symbol', dest='token_symbol', required=True, type=str, help='Token symbol')
argparser.add_argument('--decimals', dest='token_decimals', type=int, help='Token decimals')
argparser.add_argument('--sink-address', dest='sink_address', type=str, help='demurrage level,ppm per minute')
argparser.add_argument('--supply-limit', dest='supply_limit', type=int, help='token supply limit (0 = no limit)')
argparser.add_argument('--redistribution-period', type=int, help='redistribution period, minutes (0 = deactivate)') # default 10080 = week
argparser.add_argument('--multi', action='store_true', help='automatic redistribution')
#argparser.add_argument('--supply-limit', dest='supply_limit', type=int, help='token supply limit (0 = no limit)')
argparser.add_argument('--redistribution-period', dest='redistribution_period', type=int, help='redistribution period, minutes (0 = deactivate)') # default 10080 = week
#argparser.add_argument('--multi', action='store_true', help='automatic redistribution')
argparser.add_argument('--demurrage-level', dest='demurrage_level', type=int, help='demurrage level, ppm per minute')
args = argparser.parse_args()
arg_flags = chainlib.eth.cli.argflag_std_write
logg = process_log(args, logg)
extra_args = {
'redistribution_period': 'TOKEN_REDISTRIBUTION_PERIOD',
'demurrage_level': 'TOKEN_DEMURRAGE_LEVEL',
'supply_limit': 'TOKEN_SUPPLY_LIMIT',
'token_name': 'TOKEN_NAME',
'token_symbol': 'TOKEN_SYMBOL',
'token_decimals': 'TOKEN_DECIMALS',
'sink_address': 'TOKEN_SINK_ADDRESS',
'multi': None,
}
config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_fee_limit=DemurrageToken.gas(), base_config_dir=config_dir)
if not bool(config.get('TOKEN_NAME')):
logg.info('token name not set, using symbol {} as name'.format(config.get('TOKEN_SYMBOL')))
config.add(config.get('TOKEN_SYMBOL'), 'TOKEN_NAME', True)
if config.get('TOKEN_SUPPLY_LIMIT') == None:
config.add(0, 'TOKEN_SUPPLY_LIMIT', True)
if config.get('TOKEN_REDISTRIBUTION_PERIOD') == None:
config.add(10800, 'TOKEN_REDISTRIBUTION_PERIOD', True)
config = Config()
config = process_config(config, arg, args, flags)
config = process_config_local(config, arg, args, flags)
logg.debug('config loaded:\n{}'.format(config))
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
settings = ChainSettings()
settings = process_settings(settings, config)
logg.debug('settings loaded:\n{}'.format(settings))
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
signer = rpc.get_signer()
signer_address = rpc.get_sender_address()
chain_spec = settings.get('CHAIN_SPEC')
conn = settings.get('CONN')
signer = settings.get('SIGNER')
signer_address = settings.get('SENDER_ADDRESS')
gas_oracle = rpc.get_gas_oracle()
nonce_oracle = rpc.get_nonce_oracle()
gas_oracle = settings.get('FEE_ORACLE')
nonce_oracle = settings.get('NONCE_ORACLE')
c = DemurrageToken(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
settings = DemurrageTokenSettings()
settings.name = config.get('TOKEN_NAME')
settings.symbol = config.get('TOKEN_SYMBOL')
settings.decimals = int(config.get('TOKEN_DECIMALS'))
settings.demurrage_level = int(config.get('TOKEN_DEMURRAGE_LEVEL'))
settings.period_minutes = int(config.get('TOKEN_REDISTRIBUTION_PERIOD'))
settings.sink_address = config.get('TOKEN_SINK_ADDRESS')
token_settings = DemurrageTokenSettings()
token_settings.name = config.get('TOKEN_NAME')
token_settings.symbol = config.get('TOKEN_SYMBOL')
token_settings.decimals = int(config.get('TOKEN_DECIMALS'))
token_settings.demurrage_level = int(config.get('TOKEN_DEMURRAGE_LEVEL'))
token_settings.period_minutes = int(config.get('TOKEN_REDISTRIBUTION_PERIOD'))
token_settings.sink_address = config.get('TOKEN_SINK_ADDRESS')
(tx_hash_hex, o) = c.constructor(
signer_address,
settings,
token_settings,
redistribute=config.true('_MULTI'),
cap=int(config.get('TOKEN_SUPPLY_LIMIT')),
)
if config.get('_RPC_SEND'):
if settings.get('RPC_SEND'):
conn.do(o)
if config.get('_WAIT'):
if config.true('_WAIT'):
r = conn.wait(tx_hash_hex)
if r['status'] == 0:
sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you')

View File

@@ -10,7 +10,6 @@ from chainlib.eth.tx import (
from chainlib.hash import keccak256_string_to_hex
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractDecoder,
ABIContractType,
abi_decode_single,
)
@@ -311,11 +310,8 @@ class DemurrageToken(ERC20):
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionPeriod')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint104)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
@@ -325,26 +321,22 @@ class DemurrageToken(ERC20):
return o
# def to_redistribution_participants(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
# j = JSONRPCRequest(id_generator)
# o = j.template()
# o['method'] = 'eth_call'
# enc = ABIContractEncoder()
# enc.method('toRedistributionParticipants')
# v = strip_0x(redistribution)
# enc.typ_literal('(uint32,uint72,uint104)')
# #enc.typ(ABIContractType.BYTES32)
# enc.bytes32(v[:64])
# enc.bytes32(v[64:128])
# enc.bytes32(v[128:192])
# data = add_0x(enc.get())
# tx = self.template(sender_address, contract_address)
# tx = self.set_code(tx, data)
# o['params'].append(self.normalize(tx))
# o['params'].append('latest')
# o = j.finalize(o)
# return o
#
def to_redistribution_participants(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionParticipants')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def to_redistribution_supply(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
@@ -352,11 +344,8 @@ class DemurrageToken(ERC20):
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionSupply')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint104)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
@@ -372,11 +361,8 @@ class DemurrageToken(ERC20):
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionDemurrageModifier')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint104)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
@@ -466,24 +452,24 @@ class DemurrageToken(ERC20):
return self.call_noarg('supplyCap', contract_address, sender_address=sender_address)
# def grow_by(self, contract_address, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
# j = JSONRPCRequest(id_generator)
# o = j.template()
# o['method'] = 'eth_call'
# enc = ABIContractEncoder()
# enc.method('growBy')
# enc.typ(ABIContractType.UINT256)
# enc.typ(ABIContractType.UINT256)
# enc.uint256(value)
# enc.uint256(period)
# data = add_0x(enc.get())
# tx = self.template(sender_address, contract_address)
# tx = self.set_code(tx, data)
# o['params'].append(self.normalize(tx))
# o['params'].append('latest')
# o = j.finalize(o)
# return o
#
def grow_by(self, contract_address, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('growBy')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
enc.uint256(period)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def decay_by(self, contract_address, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
@@ -529,11 +515,8 @@ class DemurrageToken(ERC20):
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('getDistributionFromRedistribution')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint104)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
@@ -576,16 +559,7 @@ class DemurrageToken(ERC20):
@classmethod
def parse_redistributions(self, v):
d = ABIContractDecoder()
v = strip_0x(v)
d.typ(ABIContractType.BYTES32)
d.typ(ABIContractType.BYTES32)
d.typ(ABIContractType.BYTES32)
d.val(v[:64])
d.val(v[64:128])
d.val(v[128:192])
r = d.decode()
return ''.join(r)
return abi_decode_single(ABIContractType.BYTES32, v)
@classmethod

View File

@@ -20,7 +20,7 @@ from erc20_demurrage_token import (
DemurrageToken,
)
logg = logging.getLogger()
logg = logging.getLogger(__name__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = int(10000 * 2) # 2%
@@ -95,13 +95,6 @@ class TestDemurrage(EthTesterCase):
def setUp(self):
super(TestDemurrage, self).setUp()
# token_deploy = TestTokenDeploy()
# self.settings = token_deploy.settings
# self.sink_address = token_deploy.sink_address
# self.start_block = token_deploy.start_block
# self.start_time = token_deploy.start_time
# self.default_supply = self.default_supply
# self.default_supply_cap = self.default_supply_cap
period = PERIOD
try:
period = getattr(self, 'period')
@@ -133,6 +126,13 @@ class TestDemurrage(EthTesterCase):
logg.debug('asserted within lower {} <= {} <= {}'.format(lower_target, v, target))
def assert_within_greater(self, v, target, tolerance_ppm):
higher_target = target + (target * (tolerance_ppm / 1000000))
self.assertLessEqual(v, higher_target)
self.assertGreaterEqual(v, target)
logg.debug('asserted within lower {} <= {} <= {}'.format(target, v, higher_target))
def tearDown(self):
pass

View File

@@ -1,3 +1,3 @@
chainlib-eth>=0.1.0,<0.2.0
eth-erc20~=0.3.0
chainlib-eth~=0.4.6
eth-erc20~=0.5.1
funga-eth~=0.6.0

View File

@@ -13,6 +13,7 @@ for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_growth.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_amounts.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_single.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_burn.py
done
#modes=(SingleCap) # other contracts need to be updted

View File

@@ -1,6 +1,6 @@
[metadata]
name = erc20-demurrage-token
version = 0.1.1
version = 0.2.0
description = ERC20 token with redistributed continual demurrage
author = Louis Holbrook
author_email = dev@holbrook.no

320
python/tests/test_burn.py Normal file
View File

@@ -0,0 +1,320 @@
# standard imports
import os
import unittest
import json
import logging
import datetime
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest.base import TestDemurrage
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
TAX_LEVEL = 2
class TestBurn(TestDemurrage):
def setUp(self):
super(TestBurn, self).setUp()
def deploy(self, tax_level=None):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.mode = os.environ.get('ERC20_DEMURRAGE_TOKEN_TEST_MODE')
if self.mode == None:
self.mode = 'MultiNocap'
logg.debug('executing test setup default mode {}'.format(self.mode))
if tax_level != None:
self.deployer.settings.demurrage_level = tax_level * (10 ** 32)
self.deployer.settings.sink_address = self.accounts[9]
self.deployer.sink_address = self.accounts[9]
super(TestBurn, self).deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
# Burn tokens and immediately check balances and supply
def test_burn_basic(self):
self.deploy()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000000)
r = self.rpc.do(o)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.burn(self.address, self.accounts[1], 600000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.accounts[1])
r = self.rpc.do(o)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.burn(self.address, self.accounts[1], 600000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.assertEqual(new_supply, 400000)
o = c.total_burned(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
burned = c.parse_total_burned(r)
self.assertEqual(burned, 600000)
# burn tokens and check sink balance and supply after first redistribution period
def test_burned_redistribution(self):
self.deploy()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], 1000000000)
r = self.rpc.do(o)
(tx_hash, o) = c.burn(self.address, self.accounts[0], 500000000)
r = self.rpc.do(o)
(tx_hash, o) = c.transfer(self.address, self.accounts[0], self.sink_address, 500000000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
bal = c.parse_balance(r)
self.assertEqual(bal, 416873881) # 9 periods demurrage
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.assertEqual(new_supply, 500000000)
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
bal = c.parse_balance(r)
self.assert_within_lower(bal, 500000000, 0.0025)
self.backend.time_travel(self.start_time + (self.period_seconds * 2))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.assertEqual(new_supply, 500000000)
# if we don't burn anything more it should be the same
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
bal = c.parse_balance(r)
self.assert_within_lower(bal, 500000000, 0.0025)
# burn tokens and check sink and taxed balance and supply after first redistribution period
def test_burned_other_redistribution(self):
self.deploy()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], 1000000000)
r = self.rpc.do(o)
(tx_hash, o) = c.burn(self.address, self.accounts[0], 500000000)
r = self.rpc.do(o)
(tx_hash, o) = c.transfer(self.address, self.accounts[0], self.accounts[1], 500000000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bal = c.parse_balance(r)
self.assertEqual(bal, 416873881) # 9 periods demurrage
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.assertEqual(new_supply, 500000000)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bal = c.parse_balance(r)
self.assertEqual(bal, 408536403) # 9 periods demurrage
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
sink_bal = c.parse_balance(r)
self.assert_within_lower(sink_bal, 500000000 - 408536403, 0.09) # TODO is this ok variance, 1.0 is ppm?
self.backend.time_travel(self.start_time + (self.period_seconds * 2))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.assertEqual(new_supply, 500000000)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
next_bal = c.parse_balance(r)
self.assertEqual(next_bal, 333803985) # 9 periods demurrage
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
prev_sink_bal = sink_bal
bal = prev_sink_bal + (bal - next_bal)
sink_bal = c.parse_balance(r)
self.assert_within_lower(sink_bal, bal, 0.09) # TODO is this ok variance, 1.0 is ppm?
# verify expected results of balance and supply after multiple redistribution periods
def test_burn_accumulate(self):
self.deploy(tax_level=2/1000)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.sink_address)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.sink_address, self.default_supply)
r = self.rpc.do(o)
balance_share = int(self.default_supply / 2)
nonce_oracle = RPCNonceOracle(self.sink_address, self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.sink_address, self.accounts[1], balance_share)
r = self.rpc.do(o)
new_supply = None
burn_rate = 1000
sink_bal = None
bob_bal = None
bob_refund = None
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bob_bal = c.parse_balance(r)
prev_bob_bal = bob_bal
iterations = 100
for i in range(1, iterations + 1):
nonce_oracle = RPCNonceOracle(self.sink_address, self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
if bob_refund != None:
(tx_hash, o) = c.transfer(self.address, self.sink_address, self.accounts[1], bob_refund)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.burn(self.address, self.sink_address, burn_rate)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
self.backend.time_travel(self.start_time + (self.period_seconds * i))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bob_bal = c.parse_balance(r)
bob_refund = prev_bob_bal - bob_bal
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
burner_bal = c.parse_balance(r)
sum_supply = bob_bal + burner_bal
o = c.total_burned(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
total_burned = c.parse_balance(r)
o = c.to_base_amount(self.address, total_burned, sender_address=self.accounts[0])
r = self.rpc.do(o)
total_burned_base = c.parse_balance(r)
expected_supply = self.default_supply - (burn_rate * i)
logg.info('checking burn round {} balance burner {} bob {} supply {} expected {} summed {} burned {} base {}'.format(i, burner_bal, bob_bal, new_supply, expected_supply, sum_supply, total_burned, total_burned_base))
self.assertEqual(new_supply, expected_supply)
sum_supply = burner_bal + bob_bal
logg.debug('balances sink {} bob {} total {} supply real {} original {}'.format(sink_bal, bob_bal, sum_supply, new_supply, self.default_supply))
self.assert_within_lower(sum_supply, new_supply, 0.00001)
self.assert_within_greater(burner_bal, balance_share - total_burned, 0.1)
bob_delta = self.default_supply * ((2 / 1000000) / 1000)
self.assert_within_lower(bob_bal, balance_share - bob_delta, 0.1)
self.assertEqual(total_burned, iterations * burn_rate)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,133 @@
# standard imports
import datetime
import unittest
import logging
import os
# external imports
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
# local imports
from erc20_demurrage_token import DemurrageToken
from erc20_demurrage_token.demurrage import DemurrageCalculator
# test imports
from erc20_demurrage_token.unittest.base import TestDemurrage
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
class TestDemurragePeriods(TestDemurrage):
def setUp(self):
super(TestDemurragePeriods, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.mode = os.environ.get('ERC20_DEMURRAGE_TOKEN_TEST_MODE')
if self.mode == None:
self.mode = 'MultiNocap'
logg.debug('executing test setup default mode {}'.format(self.mode))
self.deployer.settings.demurrage_level = (2 / 1000) * (10 ** 32)
self.deployer.settings.sink_address = self.accounts[9]
self.deployer.sink_address = self.accounts[9]
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
# verify that tax level calculation is in ppm as expected
def test_ppm(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], self.default_supply)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + 60)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bob_bal = c.parse_balance(r)
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
sink_bal = c.parse_balance(r)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
balance_delta = self.default_supply * ((2 / 1000000) / 1000)
self.assertEqual(bob_bal, self.default_supply - balance_delta)
# verify balances and supply after multiple demurrage periods
def test_over_time(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], self.default_supply)
r = self.rpc.do(o)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bob_bal = c.parse_balance(r)
prev_bob_bal = bob_bal
nonce_oracle = RPCNonceOracle(self.sink_address, self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
iterations = 100
for i in range(1, iterations + 1):
self.backend.time_travel(self.start_time + (self.period_seconds * i))
(tx_hash, o) = c.transfer(self.address, self.sink_address, self.accounts[1], prev_bob_bal - bob_bal)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.apply_demurrage(self.address, self.sink_address)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bob_bal = c.parse_balance(r)
o = c.balance(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
sink_bal = c.parse_balance(r)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
logg.info('round {} supply {} balance sink {} bob {}'.format(i, new_supply, sink_bal, bob_bal))
sum_supply = sink_bal + bob_bal
bob_delta = self.default_supply * ((2 / 1000000) / 100)
self.assert_within_lower(sum_supply, new_supply, 0.00001)
self.assert_within_greater(bob_bal, self.default_supply - bob_delta, 0.001)
self.assert_within_lower(sink_bal, bob_delta, 1000)
if __name__ == '__main__':
unittest.main()

View File

@@ -28,23 +28,23 @@ testdir = os.path.dirname(__file__)
class TestGrowth(TestDemurrageDefault):
# def test_grow_by(self):
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
#
# growth_factor = (1000000 + self.tax_level) / 1000000
# v = 1000000000
# o = c.grow_by(self.address, v, 1, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# g = c.parse_grow_by(r)
# self.assertEqual(int(v * growth_factor), g)
#
# period = 10
# growth_factor = (1 + (self.tax_level) / 1000000) ** period
# o = c.grow_by(self.address, v, period, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# g = c.parse_grow_by(r)
# self.assertEqual(int(v * growth_factor), g)
def test_grow_by(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
growth_factor = (1000000 + self.tax_level) / 1000000
v = 1000000000
o = c.grow_by(self.address, v, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_grow_by(r)
self.assertEqual(int(v * growth_factor), g)
period = 10
growth_factor = (1 + (self.tax_level) / 1000000) ** period
o = c.grow_by(self.address, v, period, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_grow_by(r)
self.assertEqual(int(v * growth_factor), g)
def test_decay_by(self):
@@ -63,7 +63,7 @@ class TestGrowth(TestDemurrageDefault):
o = c.decay_by(self.address, v, period, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_decay_by(r)
self.assertEqual(int(v * growth_factor), g)
self.assertEqual(int(v * growth_factor), g)
if __name__ == '__main__':

View File

@@ -177,7 +177,6 @@ class TestRedistribution(TestDemurrageUnit):
o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
logg.debug('redistribution {}'.format(redistribution))
o = c.to_redistribution_supply(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
supply = c.parse_to_redistribution_item(r)

View File

@@ -1,18 +1,26 @@
pragma solidity >= 0.8.0;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleCap {
struct redistributionItem {
uint32 period;
uint72 value;
uint104 demurrage;
}
redistributionItem[] public redistributions; // uint51(unused) | uint64(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
// Redistribution bit field, with associated shifts and masks
// (Uses sub-byte boundaries)
bytes32[] public redistributions; // uint51(unused) | uint64(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
uint8 constant shiftRedistributionPeriod = 0;
uint256 constant maskRedistributionPeriod = 0x00000000000000000000000000000000000000000000000000000000ffffffff; // (1 << 32) - 1
uint8 constant shiftRedistributionValue = 32;
uint256 constant maskRedistributionValue = 0x00000000000000000000000000000000000000ffffffffffffffffff00000000; // ((1 << 72) - 1) << 32
uint8 constant shiftRedistributionDemurrage = 104;
uint256 constant maskRedistributionDemurrage = 0x0000000000ffffffffffffffffffffffffffff00000000000000000000000000; // ((1 << 36) - 1) << 140
// Account balances
mapping (address => uint256) account;
mapping (address => bytes32[] ) account;
uint8 constant shiftAccountValue = 0;
uint256 constant maskAccountValue = 0x0000000000000000000000000000000000000000000000ffffffffffffffffff; // (1 << 72) - 1
uint8 constant shiftAccountPeriod = 72;
uint256 constant maskAccountPeriod = 0x00000000000000000000000000000000000000ffffffff000000000000000000; // ((1 << 32) - 1) << 72
uint8 constant shiftAccountUsed = 255;
uint256 constant maskAccountUsed = 0x8000000000000000000000000000000000000000000000000000000000000000; // (1 << 255)
// Cached demurrage amount, ppm with 38 digit resolution
uint128 public demurrageAmount;
@@ -103,8 +111,6 @@ contract DemurrageTokenSingleCap {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // EIP173
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint128 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress) public {
redistributionItem memory initialRedistribution;
// ACL setup
owner = msg.sender;
minter[owner] = true;
@@ -120,7 +126,7 @@ contract DemurrageTokenSingleCap {
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(nanoDivider) * 100;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
initialRedistribution = toRedistribution(0, demurrageAmount, 0, 1);
bytes32 initialRedistribution = toRedistribution(0, demurrageAmount, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
@@ -163,43 +169,36 @@ contract DemurrageTokenSingleCap {
return (baseBalance * currentDemurragedAmount) / (nanoDivider * 1000000000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return account[_account];
uint256 lastPeriodUsed;
lastPeriodUsed = account[_account].length;
if (lastPeriodUsed == 0) {
return 0;
}
return uint256(account[_account][lastPeriodUsed]) & maskAccountValue;
}
/// 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]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
account[_account] = oldBalance + _delta;
movePeriodBalance(_account, int256(_delta));
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
account[_account] = oldBalance - _delta;
movePeriodBalance(_account, int256(_delta) * -1);
return true;
}
@@ -220,32 +219,31 @@ contract DemurrageTokenSingleCap {
}
// Deserializes the redistribution word
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) public pure returns(redistributionItem memory) {
redistributionItem memory redistribution;
// uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) public pure returns(bytes32) {
bytes32 redistribution;
redistribution.period = uint32(_period);
redistribution.value = uint72(_value);
redistribution.demurrage = uint104(_demurrageModifierPpm);
redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.period);
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & maskRedistributionPeriod;
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.value);
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.demurrage);
function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionDemurrage) >> shiftRedistributionDemurrage;
}
// Client accessor to the redistributions array length
function redistributionCount() public view returns (uint256) {
return redistributions.length;
@@ -253,14 +251,15 @@ contract DemurrageTokenSingleCap {
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
redistributionItem memory currentRedistribution;
uint256 currentRedistribution;
uint256 grownSupply;
grownSupply = totalSupply();
currentRedistribution = redistributions[redistributions.length-1];
currentRedistribution.value = uint72(grownSupply);
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (grownSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = currentRedistribution;
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true;
}
@@ -270,15 +269,14 @@ contract DemurrageTokenSingleCap {
}
// Retrieve next redistribution if the period threshold has been crossed
function checkPeriod() private view returns (redistributionItem memory) {
redistributionItem memory lastRedistribution;
redistributionItem memory emptyRedistribution;
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[lastPeriod];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return emptyRedistribution;
return bytes32(0x00);
}
return lastRedistribution;
}
@@ -290,7 +288,7 @@ contract DemurrageTokenSingleCap {
return difference / resolutionFactor;
}
function getDistributionFromRedistribution(redistributionItem memory _redistribution) public returns (uint256) {
function getDistributionFromRedistribution(bytes32 _redistribution) public returns (uint256) {
uint256 redistributionSupply;
uint256 redistributionDemurrage;
@@ -300,7 +298,7 @@ contract DemurrageTokenSingleCap {
}
// Returns the amount sent to the sink address
function applyDefaultRedistribution(redistributionItem memory _redistribution) private returns (uint256) {
function applyDefaultRedistribution(bytes32 _redistribution) private returns (uint256) {
uint256 unit;
uint256 baseUnit;
@@ -355,34 +353,69 @@ contract DemurrageTokenSingleCap {
return (block.timestamp - _target) / 60;
}
function isEmptyRedistribution(redistributionItem memory _redistribution) public pure returns(bool) {
if (_redistribution.period > 0) {
return false;
// Deserialize the pemurrage period for the given account is participating in
function accountPeriod(address _account) public view returns (uint256) {
uint256 accountPeriods;
accountPeriods = account[_account].length;
if (accountPeriods == 0) {
return 0;
}
if (_redistribution.value > 0) {
return false;
return accountPeriodBase(_account, accountPeriods - 1);
}
function accountPeriodBase(address _account, uint256 _idx) private view returns (uint256) {
return (uint256(account[_account][_idx]) & maskAccountPeriod) >> shiftAccountPeriod;
}
function movePeriodBalance(address _account, int256 _delta) private {
int256 oldBalance;
uint256 newBalance;
uint256 workAccount;
//workAccount = uint256(account[_index][_account]);
oldBalance = int256(baseBalanceOf(_account));
newBalance = uint256(oldBalance + _delta);
require(uint72(newBalance) > uint72(uint256(oldBalance)), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value
workAccount = (1 << 255);
workAccount |= ((uint32(lastPeriod) << shiftAccountPeriod) & maskAccountPeriod);
workAccount |= (baseBalanceOf(_account) & maskAccountValue);
account[_account].push(bytes32(workAccount));
}
function registerAccountPeriod(address _account, int256 _delta) public {
uint256 accountPeriods;
uint256 lastPeriodUsed;
accountPeriods = account[_account].length;
if (lastPeriodUsed == 0) {
account[_account].push(bytes32(uint256(_delta) | (1 << 255)));
return;
}
if (_redistribution.demurrage > 0) {
return false;
lastPeriodUsed = accountPeriodBase(_account, accountPeriods - 1);
if (lastPeriodUsed != lastPeriod) {
movePeriodBalance(_account, _delta);
}
return true;
}
// Recalculate the demurrage modifier for the new period
// Note that the supply for the consecutive period will be taken at the time of code execution, and thus not necessarily at the time when the redistribution period threshold was crossed.
function changePeriod() public returns (bool) {
redistributionItem memory currentRedistribution;
redistributionItem memory nextRedistribution;
redistributionItem memory lastRedistribution;
bytes32 currentRedistribution;
bytes32 nextRedistribution;
uint256 currentPeriod;
uint256 lastDemurrageAmount;
bytes32 lastRedistribution;
uint256 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 nextPeriod;
applyDemurrage();
currentRedistribution = checkPeriod();
if (isEmptyRedistribution(currentRedistribution)) {
if (currentRedistribution == bytes32(0x00)) {
return false;
}
@@ -403,18 +436,18 @@ contract DemurrageTokenSingleCap {
}
// Reverse a value reduced by demurrage by the given period to its original value
// function growBy(uint256 _value, uint256 _period) public view returns (uint256) {
// uint256 valueFactor;
// uint256 truncatedTaxLevel;
//
// valueFactor = growthResolutionFactor;
// truncatedTaxLevel = taxLevel / nanoDivider;
//
// for (uint256 i = 0; i < _period; i++) {
// valueFactor = valueFactor + ((valueFactor * truncatedTaxLevel) / growthResolutionFactor);
// }
// return (valueFactor * _value) / growthResolutionFactor;
// }
function growBy(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor;
uint256 truncatedTaxLevel;
valueFactor = growthResolutionFactor;
truncatedTaxLevel = taxLevel / nanoDivider;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor + ((valueFactor * truncatedTaxLevel) / growthResolutionFactor);
}
return (valueFactor * _value) / growthResolutionFactor;
}
// Calculate a value reduced by demurrage by the given period
function decayBy(uint256 _value, uint256 _period) public view returns (uint256) {
@@ -539,7 +572,7 @@ contract DemurrageTokenSingleCap {
// Only token minters can burn tokens
function burn(uint256 _value) public {
require(minter[msg.sender]);
require(_value <= account[msg.sender]);
require(_value <= balanceOf(msg.sender));
uint256 _delta = toBaseAmount(_value);
applyDemurrage();

View File

@@ -1,6 +1,6 @@
SOLC = /usr/bin/solc
all: single_nocap
all: multi single
multi_nocap:
$(SOLC) DemurrageTokenMultiNocap.sol --abi --evm-version byzantium | awk 'NR>3' > DemurrageTokenMultiNocap.json
@@ -33,7 +33,10 @@ test: all
python ../python/tests/test_pure.py
install: all
cp -v DemurrageToken*.json ../python/erc20_demurrage_token/data/
cp -v DemurrageToken*.bin ../python/erc20_demurrage_token/data/
#cp -v DemurrageToken*.{json,bin} ../python/erc20_demurrage_token/data/
cp -v DemurrageTokenSingleNocap.{json,bin} ../python/erc20_demurrage_token/data/
install-broken: all
cp -v DemurrageToken*.{json,bin} ../python/erc20_demurrage_token/data/
.PHONY: test install