75 Commits

Author SHA1 Message Date
lash
bcc957f861 Remove commented contract code, makefile single nocap only 2023-02-08 08:51:57 +00:00
lash
84b1a5b439 Return redistributions type correctly 2023-02-08 08:44:43 +00:00
lash
5941a6abdf Merge branch 'master' into dev-0.2.0 2022-05-30 07:51:34 +00:00
lash
c5de0e3300 Reactivate test, expose sinkaddress 2022-05-28 09:28:10 +00:00
lash
cc1a84f818 Add catch-up period test 2022-05-27 13:22:25 +00:00
lash
ee871730dc Remove unneeded demurragestart item 2022-05-27 12:53:11 +00:00
lash
31faa78346 Keep cumulative sink total in state and deduct from upcoming demurrage 2022-05-27 12:51:10 +00:00
lash
18ee9c5f9b Make tests pass 2022-05-27 12:02:27 +00:00
lash
a0557b35a0 Fix cumulative distribution calculation bug in SingleNocap 2022-05-27 11:10:31 +00:00
lash
127c67e665 Add steps option to demurrage cli 2022-05-03 18:19:28 +00:00
lash
1387451e01 Bump deps 2022-04-24 18:53:09 +00:00
William Luke
226f81fc5c Merge branch 'lash/apply-cli' into 'master'
Add apply demurrage cli tool

See merge request cicnet/erc20-demurrage-token!9
2022-03-14 12:58:45 +00:00
Louis Holbrook
370efb3192 Add apply demurrage cli tool 2022-03-14 12:58:45 +00:00
lash
3a1fb22631 Remove arg defaults 2022-03-02 13:32:31 +00:00
lash
f1a2a78eb4 Merge branch 'master' into lash/apply-cli 2022-03-02 09:03:40 +00:00
Louis Holbrook
47ee1cfa45 Merge branch 'lash/gas-safety-valve' into 'master'
bug: Wrong redistribution amount + limited demurrage apply

Closes grassrootseconomics/cic-internal-integration#276, grassrootseconomics/cic-internal-integration#273, and grassrootseconomics/cic-internal-integration#272

See merge request cicnet/erc20-demurrage-token!7
2022-03-02 09:01:50 +00:00
Louis Holbrook
db56e0d33f bug: Wrong redistribution amount + limited demurrage apply 2022-03-02 09:01:50 +00:00
Louis Holbrook
d0c02eadbf Merge branch 'lum/add-ci' into 'master'
ci: add basic ci

See merge request cicnet/erc20-demurrage-token!8
2022-03-02 08:54:20 +00:00
William Luke
ed60b5923b ci: add basic ci 2022-03-02 08:54:20 +00:00
lash
1e24ec1352 Add apply demurrage cli tool 2022-03-02 08:15:10 +00:00
nolash
a04c826ba7 Bump deps, version 2021-12-22 20:12:42 +00:00
nolash
04f50cdede Loosen dependencies 2021-12-21 10:47:38 +00:00
21d65522a8 Merge branch 'philip/bumps' into 'master'
Bumps versions of deps and dep.

See merge request cicnet/erc20-demurrage-token!6
2021-12-18 11:49:44 +00:00
130b5ea587 Bumps lib version and deps. 2021-12-18 14:47:02 +03:00
e486e9f31a Moves configs into data folder 2021-12-18 14:34:01 +03:00
959b018247 Bumps deps version for conflict resolution. 2021-12-15 09:35:08 +03:00
6ecacd60d4 Bumps lib patch version. 2021-12-15 09:34:50 +03:00
c40157318f Bumps versions of deps and dep. 2021-12-15 06:18:38 +03:00
nolash
025ef614a5 WIP test rehabilitations 2021-11-15 14:45:46 +01:00
nolash
43b3d2b488 Use explicit pre-release signer 2021-08-24 21:49:06 +02:00
nolash
0e1613c5f6 Upgrade deps 2021-07-23 11:22:11 +02:00
nolash
899efb65fc Upgrade deps 2021-07-14 13:17:45 +02:00
nolash
f84edb5f3b Include default config dir in data 2021-07-05 15:45:01 +02:00
nolash
c6b5d9a8e0 Move test base to externally importable path 2021-07-05 10:26:39 +02:00
nolash
abe82949ea Add slow demurrage calc 2021-07-04 14:52:12 +02:00
nolash
a6f53e7278 Correct demurrage emu minutes logline 2021-07-04 14:37:56 +02:00
nolash
98c460dc2f Create demurrage calc from contract 2021-07-04 12:10:01 +02:00
nolash
00bb87e3ec Add python demurrage calculator 2021-07-02 15:29:56 +02:00
nolash
294ded19f5 Move to chainlib-eth 2021-06-28 11:46:05 +02:00
nolash
2c1b7cbb1e Fix result block flag 2021-06-09 16:10:34 +02:00
nolash
d8f9fedecf Update gas budget 2021-06-09 14:54:43 +02:00
nolash
c3a6a692ed Test precision loss of 2 bytes in demurrage store in redistribution data 2021-06-08 17:41:49 +02:00
nolash
030cfdfc97 Add amounts test to test script 2021-06-08 16:42:13 +02:00
nolash
2123341fe9 Rehabilitate single nocap contract 2021-06-08 16:38:45 +02:00
nolash
606b8d6238 Add transfers test, rehabilitate single test 2021-06-08 14:58:58 +02:00
nolash
34d90b3291 WIP add transfers tests 2021-06-08 13:53:17 +02:00
nolash
a2a141dbf4 Use next redistribution object hack in apply default distribution 2021-06-08 13:06:32 +02:00
nolash
0b6d58f7af Fix demurrage in multicap 2021-06-08 12:06:34 +02:00
nolash
e894dcd3cf Add mint amounts test 2021-06-08 11:53:51 +02:00
nolash
689baa5f62 Check redistribution demurrage in two periods in test period 2021-06-08 11:20:11 +02:00
nolash
12d5711e36 Improve redistribution period test 2021-06-08 11:16:56 +02:00
nolash
0dba167af2 WIP Improve redistribution precision 2021-06-08 09:52:37 +02:00
nolash
e8781a9aa0 Add growth decay test, improve resolution in growth/decay in contract (SingleCap) 2021-06-08 07:38:10 +02:00
nolash
1b1419c03b Rename divider to nano, expand supply value bit length 2021-06-07 18:16:08 +02:00
nolash
81ec2198aa Improve resolution in fractional calc in contract 2021-06-07 17:49:22 +02:00
nolash
5f69a1d7a1 WIP avoid fractional truncation 2021-06-07 15:32:50 +02:00
nolash
dd878aa5cd Apply demurrage on minutes on periods (SingleCap only) 2021-06-07 12:08:04 +02:00
nolash
32ae98d581 Remove dead contract code in default distribution (single) 2021-06-07 09:36:31 +02:00
nolash
fb8d1e548c Add sink address in token sum to total supply 2021-06-07 09:28:08 +02:00
nolash
62d8820936 Correct magnitude in sim/example 2021-06-07 09:07:34 +02:00
nolash
e47720fa04 Add redistribution single minute demurrage test 2021-06-07 09:04:17 +02:00
nolash
399e24764a Remove redistribute calls on no redistribute sim 2021-06-06 11:52:36 +02:00
nolash
b7072fc50c Add xfail on sum demurraged balance to whole 2021-06-06 10:14:41 +02:00
nolash
b09a6f4166 Fix script bug cap 2021-06-06 10:09:26 +02:00
nolash
f7432a44b7 Bump version, add sim to package 2021-06-06 09:37:11 +02:00
nolash
c69d115965 Introduce sim example 2021-06-06 09:34:18 +02:00
nolash
2f5bb63f9a Use chain string in sim constructor 2021-06-06 06:01:35 +02:00
nolash
4e11f750e8 Revert to mine for every tx, add limit test 2021-06-06 05:57:39 +02:00
nolash
7bdd18664e WIP time travel in sim 2021-06-05 20:23:06 +02:00
nolash
e142dd0432 Add transfer, mint, balance to sim 2021-06-05 19:19:17 +02:00
nolash
64621ca9b3 Add initial sim setup, test 2021-06-05 17:59:34 +02:00
nolash
996c0224cf Rehabilitate deploy script 2021-06-05 14:03:50 +02:00
nolash
b5421cdd4e Remove remainder, particiant count from single mode 2021-06-05 12:50:31 +02:00
nolash
74ef57a6a7 Remove complex account period tracker in single mode 2021-06-05 12:39:53 +02:00
nolash
f338510a1d Remove commented code 2021-06-05 12:05:38 +02:00
49 changed files with 3006 additions and 869 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ __pycache__
gmon.out
solidity/*.json
solidity/*.bin
.venv
venv

36
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,36 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: python:3.8
# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
paths:
- .cache/pip
- venv/
before_script:
- cd ./python
- python --version # For debugging
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
test:
script:
- pip install -r requirements.txt -r test_requirements.txt --extra-index-url https://pip.grassrootseconomics.net
- bash run_tests.sh

14
python/CHANGELOG Normal file
View File

@@ -0,0 +1,14 @@
- 0.1.1
* Settable demurrage steps for apply demurrage cli tool
- 0.1.0
* Dependency upgrades
- 0.0.11
* Apply demurrage cli tool
- 0.0.10
* Settable sink address
- 0.0.9
* Correct redistribution amount for SingleNocap contract
- 0.0.2
* Move to chainlib-eth
- 0.0.1
* Interface for redistributed and non-redistributed, with or without cap

View File

@@ -1 +1 @@
include sarafu_token/data/*
include erc20_demurrage_token/data/* erc20_demurrage_token/data/config/*.ini *requirements.txt

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

@@ -0,0 +1,2 @@
[eth]
provider=http://localhost:8545

View File

@@ -0,0 +1,2 @@
[session]
chain_spec=

View File

@@ -0,0 +1,8 @@
[token]
redistribution_period=10800
demurrage_level=50
supply_limit=0
symbol=RDT
name=Redistributed Demurraged Token
decimals=6
sink_address=

View File

@@ -0,0 +1,67 @@
# standard imports
import logging
import datetime
import math
# eternal imports
from chainlib.eth.constant import ZERO_ADDRESS
# local imports
from .token import DemurrageToken
logg = logging.getLogger(__name__)
class DemurrageCalculator:
def __init__(self, interest_f_minute):
self.r_min = interest_f_minute
self.r_hour = 1 - ((1 -self.r_min) ** 60)
self.r_day = 1 - ((1 -self.r_hour) ** 24)
#self.r_week = interest_f_day ** 7
logg.info('demurrage calculator set with min {:.32f} hour {:.32f} day {:.32f}'.format(self.r_min, self.r_hour, self.r_day))
def amount_since(self, amount, timestamp):
delta = datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(timestamp)
adjusted_amount = amount * ((1 - self.r_day) ** (delta.days))
logg.debug('adjusted for {} days {} -> {}'.format(delta.days, amount, adjusted_amount))
remainder = delta.seconds
remainder_hours = math.floor(remainder / (60 * 60))
adjusted_delta = adjusted_amount * ((1 - self.r_hour) ** remainder_hours)
adjusted_amount -= (adjusted_amount - adjusted_delta)
logg.debug('adjusted for {} hours {} -> {} delta {}'.format(remainder_hours, amount, adjusted_amount, adjusted_delta))
remainder -= (remainder_hours * (60 * 60))
remainder_minutes = math.floor(remainder / 60)
adjusted_delta = adjusted_amount * ((1 - self.r_min) ** remainder_minutes)
adjusted_amount -= (adjusted_amount - adjusted_delta)
logg.debug('adjusted for {} minutes {} -> {} delta {}'.format(remainder_minutes, amount, adjusted_amount, adjusted_delta))
return adjusted_amount
def amount_since_slow(self, amount, timestamp):
delta = datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(timestamp)
remainder_minutes = math.floor(delta.total_seconds() / 60)
adjusted_amount = amount * ((1 - self.r_min) ** remainder_minutes)
logg.debug('adjusted for {} minutes {} -> {} delta {}'.format(remainder_minutes, amount, adjusted_amount, amount - adjusted_amount))
return adjusted_amount
@staticmethod
def from_contract(rpc, chain_spec, contract_address, sender_address=ZERO_ADDRESS):
c = DemurrageToken(chain_spec)
o = c.tax_level(contract_address, sender_address=sender_address)
r = rpc.do(o)
taxlevel_i = c.parse_tax_level(r)
o = c.resolution_factor(contract_address, sender_address=sender_address)
r = rpc.do(o)
divider = c.parse_resolution_factor(r)
logg.debug('taxlevel {} f {}'.format(taxlevel_i, divider))
taxlevel_f = taxlevel_i / divider
return DemurrageCalculator(taxlevel_f)

View File

@@ -0,0 +1,144 @@
"""Deploy sarafu token
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# standard imports
import sys
import os
import json
import argparse
import logging
import datetime
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,
)
from chainlib.eth.block import (
block_latest,
block_by_number,
Block,
)
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
# local imports
import erc20_demurrage_token
from erc20_demurrage_token import (
DemurrageToken,
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')
arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC
argparser = chainlib.eth.cli.ArgumentParser(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.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'))
def main():
o = block_latest()
r = conn.do(o)
block_start_number = None
try:
block_start_number = hex_to_int(r)
except TypeError:
block_start_number = int(r)
o = block_by_number(block_start_number)
r = conn.do(o)
block_start = Block(r)
block_start_timestamp = block_start.timestamp
block_start_datetime = datetime.datetime.fromtimestamp(block_start_timestamp)
gas_oracle = rpc.get_gas_oracle()
c = DemurrageToken(chain_spec, gas_oracle=gas_oracle)
o = c.demurrage_timestamp(config.get('_EXEC_ADDRESS'))
r = conn.do(o)
demurrage_timestamp = None
try:
demurrage_timestamp = hex_to_int(r)
except TypeError:
demurrage_timestamp = int(r)
demurrage_datetime = datetime.datetime.fromtimestamp(demurrage_timestamp)
total_seconds = block_start_timestamp - demurrage_timestamp
total_steps = total_seconds / 60
if total_steps < 1.0:
logg.error('only {} seconds since last demurrage application, skipping'.format(total_seconds))
return
logg.debug('block start is at {} demurrage is at {} -> {} minutes'.format(
block_start_datetime,
demurrage_datetime,
total_steps,
))
rounds = 1
if config.get('_STEPS') > 0:
rounds = math.ceil(total_steps / config.get('_STEPS'))
logg.info('will perform {} rounds of {} steps'.format(rounds, config.get('_STEPS')))
last_tx_hash = None
for i in range(rounds):
signer = rpc.get_signer()
signer_address = rpc.get_sender_address()
nonce_oracle = rpc.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'):
print(tx_hash_hex)
conn.do(o)
if config.get('_WAIT_ALL') or (i == rounds - 1 and config.get('_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')
sys.exit(1)
else:
print(o)
if __name__ == '__main__':
main()

View File

@@ -12,9 +12,10 @@ import json
import argparse
import logging
# third-party imports
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
# 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,
@@ -27,9 +28,14 @@ from chainlib.eth.gas import (
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import receipt
from chainlib.eth.constant import ZERO_ADDRESS
import chainlib.eth.cli
# local imports
from erc20_demurrage_token import DemurrageToken
import erc20_demurrage_token
from erc20_demurrage_token import (
DemurrageToken,
DemurrageTokenSettings,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
@@ -37,89 +43,79 @@ logg = logging.getLogger()
script_dir = os.path.dirname(__file__)
data_dir = os.path.join(script_dir, '..', 'data')
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', type=str, help='Web3 provider url (http only)')
argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('-d', action='store_true', help='Dump RPC calls to terminal and do not send')
argparser.add_argument('--name', type=str, help='Token name')
argparser.add_argument('--decimals', default=6, type=int, help='Token decimals')
argparser.add_argument('--gas-price', type=int, dest='gas_price', help='Override gas price')
argparser.add_argument('--nonce', type=int, help='Override transaction nonce')
argparser.add_argument('--sink-address', default=ZERO_ADDRESS, type=str, help='demurrage level,ppm per minute')
argparser.add_argument('--redistribution-period', default=10080, type=int, help='redistribution period, minutes') # default 10080 = week
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('symbol', default='SRF', type=str, help='Token symbol')
argparser.add_argument('demurrage_level', type=int, help='demurrage level, ppm per minute')
config_dir = os.path.join(data_dir, 'config')
arg_flags = chainlib.eth.cli.argflag_std_write
argparser = chainlib.eth.cli.ArgumentParser(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('--demurrage-level', dest='demurrage_level', type=int, help='demurrage level, ppm per minute')
args = argparser.parse_args()
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
arg_flags = chainlib.eth.cli.argflag_std_write
block_last = args.w
block_all = args.ww
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)
passphrase_env = 'ETH_PASSPHRASE'
if args.env_prefix != None:
passphrase_env = args.env_prefix + '_' + passphrase_env
passphrase = os.environ.get(passphrase_env)
if passphrase == None:
logg.warning('no passphrase given')
passphrase=''
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)
signer_address = None
keystore = DictKeystore()
if args.y != None:
logg.debug('loading keystore file {}'.format(args.y))
signer_address = keystore.import_keystore_file(args.y, password=passphrase)
logg.debug('now have key for signer address {}'.format(signer_address))
signer = EIP155Signer(keystore)
if config.get('TOKEN_SUPPLY_LIMIT') == None:
config.add(0, 'TOKEN_SUPPLY_LIMIT', True)
chain_spec = ChainSpec.from_chain_str(args.i)
if config.get('TOKEN_REDISTRIBUTION_PERIOD') == None:
config.add(10800, 'TOKEN_REDISTRIBUTION_PERIOD', True)
logg.debug('config loaded:\n{}'.format(config))
rpc = EthHTTPConnection(args.p)
nonce_oracle = None
if args.nonce != None:
nonce_oracle = OverrideNonceOracle(signer_address, args.nonce)
else:
nonce_oracle = RPCNonceOracle(signer_address, rpc)
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
gas_oracle = None
if args.gas_price !=None:
gas_oracle = OverrideGasOracle(price=args.gas_price, conn=rpc, code_callback=DemurrageToken.gas)
else:
gas_oracle = RPCGasOracle(rpc, code_callback=DemurrageToken.gas)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
dummy = args.d
token_name = args.name
if token_name == None:
token_name = args.symbol
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
signer = rpc.get_signer()
signer_address = rpc.get_sender_address()
gas_oracle = rpc.get_gas_oracle()
nonce_oracle = rpc.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')
(tx_hash_hex, o) = c.constructor(
signer_address,
token_name,
args.symbol,
args.decimals,
args.demurrage_level,
args.redistribution_period,
args.sink_address,
settings,
redistribute=config.true('_MULTI'),
cap=int(config.get('TOKEN_SUPPLY_LIMIT')),
)
if dummy:
print(tx_hash_hex)
print(o)
else:
rpc.do(o)
if block_last:
r = rpc.wait(tx_hash_hex)
if config.get('_RPC_SEND'):
conn.do(o)
if config.get('_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')
sys.exit(1)
@@ -130,6 +126,9 @@ def main():
else:
print(tx_hash_hex)
else:
print(o)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,2 @@
from .sim import DemurrageTokenSimulation
from .error import TxLimitException

View File

@@ -0,0 +1,2 @@
class TxLimitException(RuntimeError):
pass

View File

@@ -0,0 +1,302 @@
# standard imports
import logging
# external imports
from chainlib.chain import ChainSpec
from chainlib.eth.unittest.ethtester import create_tester_signer
from chainlib.eth.unittest.base import TestRPCConnection
from chainlib.eth.tx import (
receipt,
Tx,
)
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.gas import (
OverrideGasOracle,
Gas,
)
from chainlib.eth.address import to_checksum_address
from chainlib.eth.block import (
block_latest,
block_by_number,
block_by_hash,
)
from funga.eth.keystore.dict import DictKeystore
from funga.eth.signer import EIP155Signer
from hexathon import (
strip_0x,
add_0x,
)
# local imports
from erc20_demurrage_token import DemurrageToken
from erc20_demurrage_token.sim.error import TxLimitException
logg = logging.getLogger(__name__)
class DemurrageTokenSimulation:
def __init__(self, chain_str, settings, redistribute=True, cap=0, actors=1):
self.chain_spec = ChainSpec.from_chain_str(chain_str)
self.accounts = []
self.redistribute = redistribute
self.keystore = DictKeystore()
self.signer = EIP155Signer(self.keystore)
self.eth_helper = create_tester_signer(self.keystore)
self.eth_backend = self.eth_helper.backend
self.gas_oracle = OverrideGasOracle(limit=100000, price=1)
self.rpc = TestRPCConnection(None, self.eth_helper, self.signer)
for a in self.keystore.list():
self.accounts.append(add_0x(to_checksum_address(a)))
settings.sink_address = self.accounts[0]
self.actors = []
for i in range(actors):
idx = i % 10
address = self.keystore.new()
self.actors.append(address)
self.accounts.append(address)
nonce_oracle = RPCNonceOracle(self.accounts[idx], conn=self.rpc)
c = Gas(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.create(self.accounts[idx], address, 100000 * 1000000)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
if r['status'] != 1:
raise RuntimeError('failed gas transfer to account #{}: {} from {}'.format(i, address, self.accounts[idx]))
logg.info('added actor account #{}: {} block {}'.format(i, address, r['block_number']))
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.constructor(self.accounts[0], settings, redistribute=redistribute, cap=cap)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
if (r['status'] != 1):
raise RuntimeError('contract deployment failed')
self.address = r['contract_address']
logg.info('deployed contract to {} block {}'.format(self.address, r['block_number']))
o = block_latest()
r = self.rpc.do(o)
self.last_block = r
self.start_block = self.last_block
o = block_by_number(r)
r = self.rpc.do(o)
self.last_timestamp = r['timestamp']
self.start_timestamp = self.last_timestamp
nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc)
o = c.decimals(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.decimals = c.parse_decimals(r)
self.period_seconds = settings.period_minutes * 60
self.period = 1
self.period_txs = []
self.period_tx_limit = self.period_seconds - 1
self.sink_address = settings.sink_address
logg.info('intialized at block {} timestamp {} period {} demurrage level {} sink address {} (first address in keystore)'.format(
self.last_block,
self.last_timestamp,
settings.period_minutes,
settings.demurrage_level,
settings.sink_address,
)
)
self.eth_helper.disable_auto_mine_transactions()
self.caller_contract = DemurrageToken(self.chain_spec)
self.caller_address = self.accounts[0]
def __check_limit(self):
if self.period_tx_limit == len(self.period_txs):
raise TxLimitException('reached period tx limit {}'.format(self.period_tx_limit))
def __check_tx(self, tx_hash):
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
if rcpt['status'] == 0:
raise RuntimeError('tx {} (block {} index {}) failed'.format(tx_hash, self.last_block, rcpt['transaction_index']))
logg.debug('tx {} block {} index {} verified'.format(tx_hash, self.last_block, rcpt['transaction_index']))
def get_now(self):
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(r, include_tx=False)
r = self.rpc.do(o)
return r['timestamp']
def get_minutes(self):
t = self.get_now()
return int((t - self.start_timestamp) / 60)
def get_start(self):
return self.start_timestamp
def get_period(self):
o = self.caller_contract.actual_period(self.address, sender_address=self.caller_address)
r = self.rpc.do(o)
return self.caller_contract.parse_actual_period(r)
def get_demurrage(self):
o = self.caller_contract.demurrage_amount(self.address, sender_address=self.caller_address)
r = self.rpc.do(o)
logg.info('demrrage amount {}'.format(r))
return float(self.caller_contract.parse_demurrage_amount(r) / (10 ** 38))
def get_supply(self):
o = self.caller_contract.total_supply(self.address, sender_address=self.caller_address)
r = self.rpc.do(o)
supply = self.caller_contract.parse_total_supply(r)
return supply
def from_units(self, v):
return v * (10 ** self.decimals)
def mint(self, recipient, value):
self.__check_limit()
nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], recipient, value)
self.rpc.do(o)
self.__next_block()
self.__check_tx(tx_hash)
self.period_txs.append(tx_hash)
logg.info('mint {} tokens to {} - {}'.format(value, recipient, tx_hash))
return tx_hash
def transfer(self, sender, recipient, value):
nonce_oracle = RPCNonceOracle(sender, conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.transfer(self.address, sender, recipient, value)
self.rpc.do(o)
self.__next_block()
self.__check_tx(tx_hash)
self.period_txs.append(tx_hash)
logg.info('transfer {} tokens from {} to {} - {}'.format(value, sender, recipient, tx_hash))
return tx_hash
def balance(self, holder, base=False):
o = None
if base:
o = self.caller_contract.base_balance_of(self.address, holder, sender_address=self.caller_address)
else:
o = self.caller_contract.balance_of(self.address, holder, sender_address=self.caller_address)
r = self.rpc.do(o)
return self.caller_contract.parse_balance(r)
def __next_block(self):
hsh = self.eth_helper.mine_block()
o = block_by_hash(hsh)
r = self.rpc.do(o)
for tx_hash in r['transactions']:
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
if rcpt['status'] == 0:
raise RuntimeError('tx {} (block {} index {}) failed'.format(tx_hash, self.last_block, rcpt['transaction_index']))
logg.debug('tx {} (block {} index {}) verified'.format(tx_hash, self.last_block, rcpt['transaction_index']))
logg.debug('now at block {} timestamp {}'.format(r['number'], r['timestamp']))
def next(self):
target_timestamp = self.start_timestamp + (self.period * self.period_seconds)
logg.info('warping to {}, {} from start {}'.format(target_timestamp, target_timestamp - self.start_timestamp, self.start_timestamp))
self.last_timestamp = target_timestamp
o = block_latest()
r = self.rpc.do(o)
self.last_block = r
o = block_by_number(r)
r = self.rpc.do(o)
cursor_timestamp = r['timestamp'] + 1
nonce_oracle = RPCNonceOracle(self.accounts[2], conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
i = 0
while cursor_timestamp < target_timestamp:
logg.info('mining block on {}'.format(cursor_timestamp))
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[2])
self.rpc.do(o)
self.eth_helper.time_travel(cursor_timestamp + 60)
self.__next_block()
o = receipt(tx_hash)
r = self.rpc.do(o)
if r['status'] == 0:
raise RuntimeError('demurrage fast-forward failed on step {} timestamp {} block timestamp {} target {}'.format(i, cursor_timestamp, target_timestamp))
cursor_timestamp += 60*60 # 1 hour
o = block_by_number(r['block_number'])
b = self.rpc.do(o)
logg.info('block mined on timestamp {} (delta {}) block number {}'.format(b['timestamp'], b['timestamp'] - self.start_timestamp, b['number']))
i += 1
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[2])
self.rpc.do(o)
nonce_oracle = RPCNonceOracle(self.accounts[3], conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[3])
self.rpc.do(o)
self.eth_helper.time_travel(target_timestamp + 1)
self.__next_block()
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(self.last_block)
r = self.rpc.do(o)
self.last_block = r['number']
block_base = self.last_block
logg.info('block before demurrage execution {} {}'.format(r['timestamp'], r['number']))
if self.redistribute:
for actor in self.actors:
nonce_oracle = RPCNonceOracle(actor, conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.apply_redistribution_on_account(self.address, actor, actor)
self.rpc.do(o)
nonce_oracle = RPCNonceOracle(self.sink_address, conn=self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.gas_oracle)
(tx_hash, o) = c.apply_redistribution_on_account(self.address, self.sink_address, self.sink_address)
self.rpc.do(o)
self.__next_block()
o = block_latest()
self.last_block = self.rpc.do(o)
o = block_by_number(self.last_block)
r = self.rpc.do(o)
for tx_hash in r['transactions']:
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
if rcpt['status'] == 0:
raise RuntimeError('demurrage step failed on block {}'.format(self.last_block))
self.last_timestamp = r['timestamp']
logg.debug('next concludes at block {} timestamp {}, {} after start'.format(self.last_block, self.last_timestamp, self.last_timestamp - self.start_timestamp))
self.period += 1
self.period_txs = []
return (self.last_block, self.last_timestamp)

View File

@@ -10,11 +10,12 @@ from chainlib.eth.tx import (
from chainlib.hash import keccak256_string_to_hex
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractDecoder,
ABIContractType,
abi_decode_single,
)
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.jsonrpc import jsonrpc_template
from chainlib.jsonrpc import JSONRPCRequest
from eth_erc20 import ERC20
from hexathon import (
add_0x,
@@ -38,6 +39,15 @@ class DemurrageTokenSettings:
self.sink_address = None
def __str__(self):
return 'name {} demurrage level {} period minutes {} sink address {}'.format(
self.name,
self.demurrage_level,
self.period_minutes,
self.sink_address,
)
class DemurrageToken(ERC20):
__abi = {}
@@ -70,7 +80,7 @@ class DemurrageToken(ERC20):
@staticmethod
def gas(code=None):
return 3500000
return 4000000
@staticmethod
@@ -84,6 +94,7 @@ class DemurrageToken(ERC20):
name += 'Cap'
else:
name += 'Nocap'
logg.debug('bytecode name {}'.format(name))
return name
@@ -107,6 +118,34 @@ class DemurrageToken(ERC20):
return DemurrageToken.__bytecode[name]
def increase_allowance(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('increaseAllowance')
enc.typ(ABIContractType.ADDRESS)
enc.typ(ABIContractType.UINT256)
enc.address(address)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def decrease_allowance(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('decreaseAllowance')
enc.typ(ABIContractType.ADDRESS)
enc.typ(ABIContractType.UINT256)
enc.address(address)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def add_minter(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('addMinter')
@@ -145,8 +184,36 @@ class DemurrageToken(ERC20):
return tx
def to_base_amount(self, contract_address, value, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
def burn(self, contract_address, sender_address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('burn')
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def total_burned(self, contract_address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('totalBurned')
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_base_amount(self, contract_address, value, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toBaseAmount')
@@ -157,11 +224,13 @@ class DemurrageToken(ERC20):
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def remainder(self, contract_address, parts, whole, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
def remainder(self, contract_address, parts, whole, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('remainder')
@@ -174,11 +243,13 @@ class DemurrageToken(ERC20):
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def redistributions(self, contract_address, idx, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
def redistributions(self, contract_address, idx, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('redistributions')
@@ -189,11 +260,13 @@ class DemurrageToken(ERC20):
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def account_period(self, contract_address, address, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
def account_period(self, contract_address, address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('accountPeriod')
@@ -204,26 +277,145 @@ class DemurrageToken(ERC20):
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_period(self, contract_address, redistribution, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
def to_redistribution(self, contract_address, participants, demurrage_modifier_ppm, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionPeriod')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
enc.method('toRedistribution')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(participants)
enc.uint256(demurrage_modifier_ppm)
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 apply_demurrage(self, contract_address, sender_address):
return self.transact_noarg('applyDemurrage', contract_address, sender_address)
def to_redistribution_period(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('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])
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')
# 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_supply(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('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])
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_demurrage_modifier(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('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])
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 base_balance_of(self, contract_address, address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('baseBalanceOf')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
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 apply_demurrage(self, contract_address, sender_address, limit=0, tx_format=TxFormat.JSONRPC):
if limit == 0:
return self.transact_noarg('applyDemurrage', contract_address, sender_address)
enc = ABIContractEncoder()
enc.method('applyDemurrageLimited')
enc.typ(ABIContractType.UINT256)
enc.uint256(limit)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def change_period(self, contract_address, sender_address):
@@ -240,7 +432,15 @@ class DemurrageToken(ERC20):
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def tax_level(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('taxLevel', contract_address, sender_address=sender_address)
def resolution_factor(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('resolutionFactor', contract_address, sender_address=sender_address)
def actual_period(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('actualPeriod', contract_address, sender_address=sender_address)
@@ -258,10 +458,92 @@ class DemurrageToken(ERC20):
return self.call_noarg('demurrageAmount', contract_address, sender_address=sender_address)
def demurrage_timestamp(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('demurrageTimestamp', contract_address, sender_address=sender_address)
def supply_cap(self, contract_address, sender_address=ZERO_ADDRESS):
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 decay_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('decayBy')
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 get_distribution(self, contract_address, supply, demurrage_amount, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('getDistribution')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(supply)
enc.uint256(demurrage_amount)
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 get_distribution_from_redistribution(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('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])
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
@classmethod
def parse_actual_period(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@@ -294,7 +576,16 @@ class DemurrageToken(ERC20):
@classmethod
def parse_redistributions(self, v):
return abi_decode_single(ABIContractType.BYTES32, 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)
@classmethod
@@ -307,6 +598,41 @@ class DemurrageToken(ERC20):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_to_redistribution_item(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_supply_cap(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_grow_by(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_decay_by(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_get_distribution(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_tax_level(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_resolution_factor(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_total_burned(self, v):
return abi_decode_single(ABIContractType.UINT256, v)

View File

@@ -0,0 +1 @@
from .base import *

View File

@@ -12,6 +12,7 @@ from chainlib.eth.block import (
block_by_number,
)
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.constant import ZERO_ADDRESS
# local imports
from erc20_demurrage_token import (
@@ -19,80 +20,119 @@ from erc20_demurrage_token import (
DemurrageToken,
)
logg = logging.getLogger()
#BLOCKTIME = 5 # seconds
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 = 1
PERIOD = 10
class TestTokenDeploy:
class TestDemurrage(EthTesterCase):
def __init__(self, rpc, token_symbol='FOO', token_name='Foo Token', sink_address=ZERO_ADDRESS, supply=10**12, tax_level=TAX_LEVEL, period=PERIOD):
self.tax_level = tax_level
self.period_seconds = period * 60
def setUp(self):
super(TestDemurrage, self).setUp()
self.tax_level = TAX_LEVEL
self.period_seconds = PERIOD * 60
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
self.settings = DemurrageTokenSettings()
self.settings.name = 'Foo Token'
self.settings.symbol = 'FOO'
self.settings.name = token_name
self.settings.symbol = token_symbol
self.settings.decimals = 6
self.settings.demurrage_level = TAX_LEVEL * (10 ** 32)
self.settings.period_minutes = PERIOD
self.settings.sink_address = self.accounts[9]
self.settings.demurrage_level = tax_level * (10 ** 32)
self.settings.period_minutes = period
self.settings.sink_address = sink_address
self.sink_address = self.settings.sink_address
logg.debug('using demurrage token settings: {}'.format(self.settings))
o = block_latest()
self.start_block = self.rpc.do(o)
self.start_block = rpc.do(o)
o = block_by_number(self.start_block, include_tx=False)
r = self.rpc.do(o)
r = rpc.do(o)
try:
self.start_time = int(r['timestamp'], 16)
except TypeError:
self.start_time = int(r['timestamp'])
self.default_supply = 1000000000000
self.default_supply = supply
self.default_supply_cap = int(self.default_supply * 10)
def deploy(self, interface, mode):
def deploy(self, rpc, deployer_address, interface, mode, supply_cap=10**12):
tx_hash = None
o = None
logg.debug('mode {} {}'.format(mode, self.settings))
self.mode = mode
if mode == 'MultiNocap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=True, cap=0)
(tx_hash, o) = interface.constructor(deployer_address, self.settings, redistribute=True, cap=0)
elif mode == 'SingleNocap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=False, cap=0)
(tx_hash, o) = interface.constructor(deployer_address, self.settings, redistribute=False, cap=0)
elif mode == 'MultiCap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=True, cap=self.default_supply_cap)
(tx_hash, o) = interface.constructor(deployer_address, self.settings, redistribute=True, cap=supply_cap)
elif mode == 'SingleCap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=False, cap=self.default_supply_cap)
(tx_hash, o) = interface.constructor(deployer_address, self.settings, redistribute=False, cap=supply_cap)
else:
raise ValueError('Invalid mode "{}", valid are {}'.format(self.mode, DemurrageToken.valid_modes))
raise ValueError('Invalid mode "{}", valid are {}'.format(mode, DemurrageToken.valid_modes))
r = self.rpc.do(o)
r = rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
r = rpc.do(o)
assert r['status'] == 1
self.start_block = r['block_number']
self.address = r['contract_address']
o = block_by_number(r['block_number'])
r = self.rpc.do(o)
r = rpc.do(o)
self.start_time = r['timestamp']
return self.address
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')
except AttributeError as e:
pass
self.deployer = TestTokenDeploy(self.rpc, period=period)
self.default_supply = self.deployer.default_supply
self.default_supply_cap = self.deployer.default_supply_cap
self.start_block = None
self.address = None
self.start_time = None
def deploy(self, interface, mode):
self.address = self.deployer.deploy(self.rpc, self.accounts[0], interface, mode, supply_cap=self.default_supply_cap)
self.start_block = self.deployer.start_block
self.start_time = self.deployer.start_time
self.tax_level = self.deployer.tax_level
self.period_seconds = self.deployer.period_seconds
self.sink_address = self.deployer.sink_address
logg.debug('contract address {} start block {} start time {}'.format(self.address, self.start_block, self.start_time))
def assert_within_lower(self, v, target, tolerance_ppm):
lower_target = target - (target * (tolerance_ppm / 1000000))
self.assertGreaterEqual(v, lower_target)
self.assertLessEqual(v, target)
logg.debug('asserted within lower {} <= {} <= {}'.format(lower_target, v, target))
def tearDown(self):
pass
@@ -163,3 +203,57 @@ class TestDemurrageCap(TestDemurrage):
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
class TestDemurrageUnit(TestDemurrage):
def setUp(self):
self.period = 1
self.period_seconds = self.period * 60
self.tax_level = TAX_LEVEL
super(TestDemurrageUnit, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
self.settings = DemurrageTokenSettings()
self.settings.name = 'Foo Token'
self.settings.symbol = 'FOO'
self.settings.decimals = 6
self.settings.demurrage_level = self.tax_level * (10 ** 32)
self.settings.period_minutes = int(self.period_seconds/60)
self.settings.sink_address = self.accounts[9]
self.sink_address = self.settings.sink_address
o = block_latest()
self.start_block = self.rpc.do(o)
o = block_by_number(self.start_block, include_tx=False)
r = self.rpc.do(o)
try:
self.start_time = int(r['timestamp'], 16)
except TypeError:
self.start_time = int(r['timestamp'])
self.default_supply = 1000000000000
self.default_supply_cap = int(self.default_supply * 10)
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')
unit_valid_modes = [
'SingleNocap',
'SingleCap',
]
if self.mode != None:
if self.mode not in unit_valid_modes:
raise ValueError('Invalid mode "{}" for "unit" contract tests, valid are {}'.format(self.mode, unit_valid_modes))
else:
self.mode = 'SingleNocap'
logg.debug('executing test setup unit mode {}'.format(self.mode))
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))

76
python/examples/sim.py Normal file
View File

@@ -0,0 +1,76 @@
# standard imports
import logging
# local imports
from erc20_demurrage_token import DemurrageTokenSettings
from erc20_demurrage_token.sim import DemurrageTokenSimulation
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
decay_per_minute = 0.000050105908373373 # equals approx 2% per month
# parameters for simulation object
settings = DemurrageTokenSettings()
settings.name = 'Simulated Demurrage Token'
settings.symbol = 'SIM'
settings.decimals = 6
settings.demurrage_level = int(decay_per_minute*(10**40))
settings.period_minutes = 10800 # 1 week in minutes
chain = 'evm:foochain:42'
cap = (10 ** 6) * (10 ** 12)
# instantiate simulation
sim = DemurrageTokenSimulation(chain, settings, redistribute=True, cap=cap, actors=10)
# name the usual suspects
alice = sim.actors[0]
bob = sim.actors[1]
carol = sim.actors[2]
# mint and transfer (every single action advances one block, and one second in time)
sim.mint(alice, sim.from_units(100)) # 10000000 tokens
sim.mint(bob, sim.from_units(100))
sim.transfer(alice, carol, sim.from_units(50))
# check that balances have been updated
assert sim.balance(alice) == sim.from_units(50)
assert sim.balance(bob) == sim.from_units(100)
assert sim.balance(carol) == sim.from_units(50)
# advance to next redistribution period
sim.next()
# inspect balances
print('alice balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(alice), sim.balance(alice, base=True)))
print('bob balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(bob), sim.balance(bob, base=True)))
print('carol balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(carol), sim.balance(carol, base=True)))
print('sink balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(sim.sink_address), sim.balance(sim.sink_address, base=True)))
# get times
minutes = sim.get_minutes()
start = sim.get_now()
timestamp = sim.get_start()
period = sim.get_period()
print('start {} now {} period {} minutes passed {}'.format(start, timestamp, period, minutes))
contract_demurrage = 1 - sim.get_demurrage_modifier() # demurrage in percent (float)
frontend_demurrage = ((1 - decay_per_minute) ** minutes / 100) # corresponding demurrage modifier (float)
demurrage_delta = contract_demurrage - frontend_demurrage # difference between demurrage in contract and demurrage calculated in frontend
alice_checksum = 50000000 - (50000000 * frontend_demurrage) + (200000000 * frontend_demurrage) # alice's balance calculated with frontend demurrage
print("""alice frontend balance {}
alice contract balance {}
frontend demurrage {}
contract demurrage {}
demurrage delta {}""".format(
alice_checksum,
sim.balance(alice),
frontend_demurrage,
contract_demurrage,
demurrage_delta),
)
balance_sum = sim.balance(alice) + sim.balance(bob) + sim.balance(carol)
print('sum of contract demurraged balances {}'.format(balance_sum))

View File

@@ -0,0 +1,80 @@
# standard imports
import logging
# local imports
from erc20_demurrage_token import DemurrageTokenSettings
from erc20_demurrage_token.sim import DemurrageTokenSimulation
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
decay_per_minute = 0.00000050105908373373 # equals approx 2% per month
# parameters for simulation object
settings = DemurrageTokenSettings()
settings.name = 'Simulated Demurrage Token'
settings.symbol = 'SIM'
settings.decimals = 6
settings.demurrage_level = int(decay_per_minute*(10**38))
#settings.period_minutes = 1 # 1 week in minutes
settings.period_minutes = 60*24*7
chain = 'evm:foochain:42'
cap = (10 ** 6) * (10 ** 12)
#cap = 0
# instantiate simulation
sim = DemurrageTokenSimulation(chain, settings, redistribute=False, cap=cap, actors=10)
# name the usual suspects
alice = sim.actors[0]
bob = sim.actors[1]
carol = sim.actors[2]
# mint and transfer (every single action advances one block, and one second in time)
sim.mint(alice, sim.from_units(100)) # 10000000 tokens
sim.mint(bob, sim.from_units(100))
sim.transfer(alice, carol, sim.from_units(50))
# check that balances have been updated
#assert sim.balance(alice) == sim.from_units(50)
#assert sim.balance(bob) == sim.from_units(100)
#assert sim.balance(carol) == sim.from_units(50)
# advance to next redistribution period
sim.next()
# inspect balances
print('alice balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(alice), sim.balance(alice, base=True)))
print('bob balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(bob), sim.balance(bob, base=True)))
print('carol balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(carol), sim.balance(carol, base=True)))
print('sink balance: demurraged {:>9d} base {:>9d}'.format(sim.balance(sim.sink_address), sim.balance(sim.sink_address, base=True)))
# get times
minutes = sim.get_minutes()
timestamp = sim.get_now()
start = sim.get_start()
period = sim.get_period()
print('start {} now {} period {} minutes passed {}'.format(start, timestamp, period, minutes))
contract_demurrage = 1 - sim.get_demurrage() # demurrage in percent (float)
frontend_demurrage = 1.0 - ((1 - decay_per_minute) ** minutes) # corresponding demurrage modifier (float)
demurrage_delta = contract_demurrage - frontend_demurrage # difference between demurrage in contract and demurrage calculated in frontend
alice_checksum = 50000000 - (50000000 * frontend_demurrage) + (200000000 * frontend_demurrage) # alice's balance calculated with frontend demurrage
#print("""alice frontend balance {}
print("""alice contract balance {}
frontend demurrage {:.38f}
contract demurrage {:.38f}
demurrage delta {:.38f}""".format(
alice_checksum,
sim.balance(alice),
frontend_demurrage,
contract_demurrage,
demurrage_delta),
)
balance_sum = sim.balance(alice) + sim.balance(bob) + sim.balance(carol) + sim.balance(sim.sink_address)
supply = sim.get_supply()
print('sum of contract demurraged balances {}'.format(balance_sum))
print('total token supply {}'.format(supply))

View File

@@ -1,3 +1,3 @@
chainlib~=0.0.3rc3
eth-erc20~=0.0.9a4
crypto-dev-signer~=0.4.14b3
chainlib-eth>=0.1.0,<0.2.0
eth-erc20~=0.3.0
funga-eth~=0.6.0

43
python/run_tests.sh Normal file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
set -x
set -e
export PYTHONPATH=.
#modes=(MultiNocap MultiCap SingleCap SingleNocap)
#modes=(SingleCap SingleNocap) # other contracts need to be updted
modes=(SingleNocap) # other contracts need to be updted
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_basic.py
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
done
#modes=(SingleCap) # other contracts need to be updted
modes=()
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_period.py
done
modes=(SingleNocap) # other contracts need to be updted
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_redistribution_unit.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_redistribution_single.py
done
#modes=(MultiCap SingleCap)
modes=()
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_cap.py
done
#modes=(MultiCap MultiNocap)
#for m in ${modes[@]}; do
# ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_remainder.py
# ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_redistribution.py
#done
set +e
set +x

View File

@@ -1,10 +1,10 @@
[metadata]
name = erc20-demurrage-token
version = 0.0.1b1
version = 0.1.1
description = ERC20 token with redistributed continual demurrage
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/grassrootseconomics/sarafu-token
url = https://gitlab.com/ccicnet/erc20-demurrage-token
keywords =
ethereum
blockchain
@@ -25,10 +25,13 @@ licence_files =
[options]
include_package_data = True
python_requires = >= 3.6
python_requires = >= 3.7
packages =
erc20_demurrage_token
erc20_demurrage_token.runnable
erc20_demurrage_token.data
erc20_demurrage_token.sim
erc20_demurrage_token.unittest
[options.package_data]
* =
@@ -38,3 +41,4 @@ packages =
[options.entry_points]
console_scripts =
erc20-demurrage-token-deploy = erc20_demurrage_token.runnable.deploy:main
erc20-demurrage-token-apply = erc20_demurrage_token.runnable.apply:main

View File

@@ -1,29 +0,0 @@
#!/bin/bash
set -e
export PYTHONPATH=.
modes=(MultiNocap MultiCap SingleCap SingleNocap)
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_pure.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_period.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_basic.py
done
modes=(MultiCap SingleCap)
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_cap.py
done
modes=(SingleCap SingleNocap)
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_single.py
done
modes=(MultiCap MultiNocap)
for m in ${modes[@]}; do
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_redistribution.py
done
set +e

View File

@@ -1,3 +1,2 @@
#web3==5.12.2
eth_tester==0.5.0b3
py-evm==0.3.0a20

View File

@@ -0,0 +1,73 @@
# standard imports
import unittest
import logging
# local imports
from erc20_demurrage_token import DemurrageTokenSettings
from erc20_demurrage_token.sim import DemurrageTokenSimulation
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
class TestSim(unittest.TestCase):
def setUp(self):
self.cap = 0
settings = DemurrageTokenSettings()
settings.name = 'Simulated Demurrage Token'
settings.symbol = 'SIM'
settings.decimals = 6
settings.demurrage_level = 5010590837337300000000000000000000 # equals approx 2% per month
settings.period_minutes = 10800 # 1 week in minutes
self.sim = DemurrageTokenSimulation('evm:foochain:42', settings, redistribute=True, cap=self.cap, actors=10)
def test_mint(self):
self.sim.mint(self.sim.actors[0], 1024)
self.sim.next()
balance = self.sim.balance(self.sim.actors[0])
self.assertEqual(balance, 1023)
def test_transfer(self):
self.sim.mint(self.sim.actors[0], 1024)
self.sim.transfer(self.sim.actors[0], self.sim.actors[1], 500)
self.sim.next()
balance = self.sim.balance(self.sim.actors[0])
self.assertEqual(balance, 523)
balance = self.sim.balance(self.sim.actors[1])
self.assertEqual(balance, 499)
def test_more_periods(self):
self.sim.mint(self.sim.actors[0], 1024)
self.sim.mint(self.sim.actors[1], 1024)
self.sim.next()
self.sim.mint(self.sim.actors[0], 1024)
self.sim.next()
balance = self.sim.balance(self.sim.actors[0])
self.assertEqual(balance, 2047)
def test_demurrage(self):
self.sim.mint(self.sim.actors[0], self.sim.from_units(100))
self.sim.mint(self.sim.actors[1], self.sim.from_units(100))
self.sim.transfer(self.sim.actors[0], self.sim.actors[2], self.sim.from_units(10))
self.sim.next()
balance = self.sim.balance(self.sim.actors[0])
self.assertEqual(balance, 90005520)
balance = self.sim.balance(self.sim.actors[1])
self.assertEqual(balance, 99995000)
balance = self.sim.balance(self.sim.actors[1], base=True)
self.assertEqual(balance, 100000000)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,35 @@
# standard imports
import unittest
import logging
# local imports
from erc20_demurrage_token import DemurrageTokenSettings
from erc20_demurrage_token.sim import (
DemurrageTokenSimulation,
TxLimitException,
)
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
class TestLimit(unittest.TestCase):
def setUp(self):
self.cap = 0
settings = DemurrageTokenSettings()
settings.name = 'Simulated Demurrage Token'
settings.symbol = 'SIM'
settings.decimals = 6
settings.demurrage_level = 1
settings.period_minutes = 1
self.sim = DemurrageTokenSimulation('evm:foochain:42', settings, redistribute=True, cap=self.cap, actors=1)
def test_limit(self):
with self.assertRaises(TxLimitException):
for i in range(60):
self.sim.mint(self.sim.actors[0], i)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,123 @@
# standard imports
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestAmounts(TestDemurrageDefault):
def test_mints(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], 1000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 817)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000)
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, 1817, 750)
self.backend.time_travel(self.start_time + self.period_seconds * 2)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
expected_balance = ((1 - self.tax_level / 1000000) ** 10) * 1000
expected_balance += ((1 - self.tax_level / 1000000) ** 20) * 1000
self.assert_within_lower(balance, expected_balance, 500)
def test_transfers(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], 2000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 1634)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], self.accounts[2], 500)
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 1134)
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, 500, 2000)
def test_dynamic_amount(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], 2000)
r = self.rpc.do(o)
cases = [
(61, 1960),
(121, 1920),
(181, 1882),
(241, 1844),
(301, 1807),
(361, 1771),
(421, 1736),
(481, 1701),
(541, 1667),
(601, 1634),
]
for case in cases:
self.backend.time_travel(self.start_time + case[0])
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, case[1])
if __name__ == '__main__':
unittest.main()

View File

@@ -9,12 +9,16 @@ import datetime
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 tests.base import TestDemurrageDefault
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -35,8 +39,23 @@ class TestBasic(TestDemurrageDefault):
r = self.rpc.do(o)
def test_apply_demurrage(self):
modifier = 10 * (10 ** 37)
def test_balance(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], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 1024)
def test_apply_demurrage_limited(self):
modifier = (10 ** 28)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
@@ -46,21 +65,81 @@ class TestBasic(TestDemurrageDefault):
demurrage_amount = c.parse_demurrage_amount(r)
self.assertEqual(modifier, demurrage_amount)
self.backend.time_travel(self.start_time + self.period_seconds - 1)
self.backend.time_travel(self.start_time + 120)
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0], limit=1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
modifier_base = 1000000 - self.tax_level
modifier = int(modifier_base * (10 ** 22)) # 38 decimal places minus 6 (1000000)
self.assertEqual(modifier, demurrage_amount)
def test_apply_demurrage(self):
modifier = (10 ** 28)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
self.assertEqual(modifier, demurrage_amount)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(r)
b = self.rpc.do(o)
logg.debug('block {} start {}'.format(b['timestamp'], self.start_time))
self.backend.time_travel(self.start_time + 2)
(tx_hash, o) = c.apply_demurrage(self.address, sender_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.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
modifier = int(98 * (10 ** 36))
self.assertEqual(modifier, demurrage_amount)
self.backend.time_travel(self.start_time + 61)
(tx_hash, o) = c.apply_demurrage(self.address, sender_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.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
modifier_base = 1000000 - self.tax_level
modifier = int(modifier_base * (10 ** 22)) # 38 decimal places minus 6 (1000000)
self.assertEqual(modifier, demurrage_amount)
self.backend.time_travel(self.start_time + 601)
(tx_hash, o) = c.apply_demurrage(self.address, sender_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.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
modifier_base = ((1000000 - self.tax_level) / 1000000) ** 10
logg.warning('mod base {}'.format(modifier_base))
modifier = int(modifier_base * (10 ** 12))
rounding_tolerance_nano = 4000000 # 0.000004% precision
demurrage_amount_truncate = int(demurrage_amount / (10 ** 16)) # equals 38 decimal places - 14 for the modifier magniture - 2 for percent int calc + 6 for token decimals <- TODO verify this calc
self.assertGreaterEqual(modifier, demurrage_amount_truncate - rounding_tolerance_nano)
self.assertLessEqual(modifier, demurrage_amount_truncate)
def test_mint(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
@@ -197,7 +276,52 @@ class TestBasic(TestDemurrageDefault):
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
def test_approve(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.approve(self.address, self.accounts[0], self.accounts[1], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 600)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 0)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 600)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.increase_allowance(self.address, self.accounts[0], self.accounts[1], 200)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.decrease_allowance(self.address, self.accounts[0], self.accounts[1], 800)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 42)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
def test_transfer_from(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
@@ -236,6 +360,12 @@ class TestBasic(TestDemurrageDefault):
balance = c.parse_balance_of(r)
self.assertEqual(balance, 500)
(tx_hash, o) = c.transfer_from(self.address, self.accounts[2], self.accounts[1], self.accounts[3], 1)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -18,7 +18,7 @@ from hexathon import (
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageCap
from erc20_demurrage_token.unittest.base import TestDemurrageCap
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()

View File

@@ -0,0 +1,41 @@
# standard imports
import datetime
import unittest
# external imports
from chainlib.eth.nonce import RPCNonceOracle
# 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
class TestEmulate(TestDemurrage):
def test_amount_since(self):
d = datetime.datetime.utcnow() - datetime.timedelta(seconds=29, hours=5, minutes=3, days=4)
c = DemurrageCalculator(0.00000050105908373373)
a = c.amount_since(100, d.timestamp())
self.assert_within_lower(a, 99.69667, 0.1)
def test_amount_since_slow(self):
d = datetime.datetime.utcnow() - datetime.timedelta(seconds=29, hours=5, minutes=3, days=4)
c = DemurrageCalculator(0.00000050105908373373)
a = c.amount_since_slow(100, d.timestamp())
self.assert_within_lower(a, 99.69667, 0.1)
def test_from_contract(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.deploy(c, 'SingleNocap')
dc = DemurrageCalculator.from_contract(self.rpc, self.chain_spec, self.address, sender_address=self.accounts[0])
self.assertEqual(dc.r_min, 0.02)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,70 @@
# 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 TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
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_decay_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.decay_by(self.address, v, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_decay_by(r)
self.assertEqual(int(v * growth_factor), g)
period = 10
growth_factor = (1 - (self.tax_level) / 1000000) ** period
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)
if __name__ == '__main__':
unittest.main()

View File

@@ -7,13 +7,21 @@ import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.tx import (
receipt,
TxFactory,
TxFormat,
)
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageDefault
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -31,7 +39,7 @@ class TestPeriod(TestDemurrageDefault):
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + 61)
self.backend.time_travel(self.start_time + self.period_seconds)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
@@ -63,6 +71,130 @@ class TestPeriod(TestDemurrageDefault):
period = c.parse_actual_period(r)
self.assertEqual(2, period)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_item(r)
# allow test code float rounding error to billionth
modifier = (1 - (self.tax_level / 1000000)) ** (self.period_seconds / 60)
modifier *= 10 ** 9
modifier = int(modifier) * (10 ** (28 - 9))
period /= (10 ** (28 - 9))
period = int(period) * (10 ** (28 - 9))
self.assertEqual(modifier, period)
self.backend.time_travel(self.start_time + self.period_seconds * 2)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(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.redistributions(self.address, 2, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution = c.parse_redistributions(r)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_item(r)
# allow test code float rounding error to billionth
modifier = (1 - (self.tax_level / 1000000)) ** ((self.period_seconds * 2) / 60)
modifier *= 10 ** 9
modifier = int(modifier) * (10 ** (28 - 9))
period /= (10 ** (28 - 9))
period = int(period) * (10 ** (28 - 9))
self.assertEqual(modifier, period)
def test_change_sink(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.balance_of(self.address, ZERO_ADDRESS, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 0)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 102400000000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(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.balance_of(self.address, ZERO_ADDRESS, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertGreater(balance, 0)
old_sink_balance = balance
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 0)
nonce_oracle = RPCNonceOracle(self.accounts[5], self.rpc)
c = TxFactory(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
enc = ABIContractEncoder()
enc.method('setSinkAddress')
enc.typ(ABIContractType.ADDRESS)
enc.address(self.accounts[3])
data = enc.get()
o = c.template(self.accounts[5], self.address, use_nonce=True)
o = c.set_code(o, data)
(tx_hash, o) = c.finalize(o, TxFormat.JSONRPC)
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 = TxFactory(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
enc = ABIContractEncoder()
enc.method('setSinkAddress')
enc.typ(ABIContractType.ADDRESS)
enc.address(self.accounts[3])
data = enc.get()
o = c.template(self.accounts[0], self.address, use_nonce=True)
o = c.set_code(o, data)
(tx_hash, o) = c.finalize(o, TxFormat.JSONRPC)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + (self.period_seconds * 2) + 1)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(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.balance_of(self.address, ZERO_ADDRESS, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertLess(balance, old_sink_balance)
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertGreater(balance, 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -8,7 +8,10 @@ import logging
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
from chainlib.eth.block import (
block_latest,
block_by_number,
)
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
@@ -19,245 +22,307 @@ from hexathon import (
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageDefault
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistribution(TestDemurrageDefault):
def test_debug_periods(self):
def test_whole_is_parts(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pactual = c.parse_actual_period(r)
o = c.period_start(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pstart = c.parse_actual_period(r)
o = c.period_duration(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pduration = c.parse_actual_period(r)
o = block_latest()
blocknumber = self.rpc.do(o)
logg.debug('actual {} start {} duration {} blocknumber {}'.format(pactual, pstart, pduration, blocknumber))
# TODO: check receipt log outputs
def test_redistribution_storage(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertEqual(strip_0x(r), '000000000000000000000000f424000000000000000000000000000000000001')
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000000)
r = self.rpc.do(o)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1000000)
r = self.rpc.do(o)
external_address = to_checksum_address('0x' + os.urandom(20).hex())
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[2], external_address, 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.transfer(self.address, self.accounts[1], external_address, 999999)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertEqual(strip_0x(r), '000000000000000000000000f42400000000010000000000001e848000000001')
o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertEqual(strip_0x(r), '000000000000000000000000f42400000000010000000000001e848000000001')
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], 1000000)
r = self.rpc.do(o)
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertEqual(strip_0x(r), '000000000000000000000000ef4200000000000000000000002dc6c000000002')
def test_redistribution_balance_on_zero_participants(self):
supply = self.default_supply
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], supply)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
self.rpc.do(o)
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
self.assertEqual(rcpt['status'], 1)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 100000000)
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])
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 100000000)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
total_supply = c.parse_total_supply(r)
sink_increment = int(total_supply * (self.tax_level / 1000000))
self.assertEqual(supply, total_supply)
self.assertEqual(r['status'], 1)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], self.accounts[3], 50000000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
for l in rcpt['logs']:
if l['topics'][0] == '0xa0717e54e02bd9829db5e6e998aec0ae9de796b8d150a3cc46a92ab869697755': # event Decayed(uint256,uint256,uint256,uint256)
period = int.from_bytes(bytes.fromhex(strip_0x(l['topics'][1])), 'big')
self.assertEqual(period, 2)
b = bytes.fromhex(strip_0x(l['data']))
remainder = int.from_bytes(b, 'big')
self.assertEqual(remainder, int((1000000 - self.tax_level) * (10 ** 32)))
self.backend.time_travel(self.start_time + self.period_seconds + 1)
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(r)
r = self.rpc.do(o)
self.assertEqual(r['timestamp'], self.start_time + self.period_seconds)
(tx_hash, o) = c.change_period(self.address, self.accounts[1])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.apply_redistribution_on_account(self.address, self.accounts[1], self.accounts[1])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
balance = 0
for i in range(3):
o = c.balance_of(self.address, self.accounts[i+1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_item = c.parse_balance_of(r)
balance += balance_item
logg.debug('balance {} {} total {}'.format(i, balance_item, balance))
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
sink_balance = c.parse_balance_of(r)
balance_item = c.parse_balance_of(r)
balance += balance_item
self.assertEqual(sink_balance, int(sink_increment * 0.98))
self.assertEqual(sink_balance, int(sink_increment * (1000000 - self.tax_level) / 1000000))
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, supply - sink_increment)
def test_redistribution_two_of_ten(self):
mint_amount = 100000000
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
z = 0
for i in range(10):
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i], mint_amount)
self.rpc.do(o)
z += mint_amount
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
initial_balance = c.parse_balance_of(r)
spend_amount = 1000000
external_address = to_checksum_address('0x' + os.urandom(20).hex())
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], external_address, spend_amount)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[2], external_address, spend_amount)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
# No cheating!
nonce_oracle = RPCNonceOracle(self.accounts[3], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[3], self.accounts[3], spend_amount)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
# No cheapskating!
nonce_oracle = RPCNonceOracle(self.accounts[4], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[4], external_address, spend_amount-1)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[4])
self.rpc.do(o)
(tx_hash, o) = c.change_period(self.address, self.accounts[4])
self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
bummer_balance = c.parse_balance_of(r)
self.assertEqual(bummer_balance, mint_amount - (mint_amount * (self.tax_level / 1000000)))
logg.debug('bal {} '.format(bummer_balance))
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
bummer_balance = c.parse_balance_of(r)
spender_balance = mint_amount - spend_amount
spender_decayed_balance = int(spender_balance - (spender_balance * (self.tax_level / 1000000)))
self.assertEqual(bummer_balance, spender_decayed_balance)
logg.debug('bal {} '.format(bummer_balance))
(tx_hash, o) = c.apply_redistribution_on_account(self.address, self.accounts[4], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
# logg.debug('log {}'.format(r.logs))
(tx_hash, o) = c.apply_redistribution_on_account(self.address, self.accounts[4], self.accounts[2])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution_data = c.parse_redistributions(r)
logg.debug('redist data {}'.format(redistribution_data))
o = c.account_period(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
account_period_data = c.parse_account_period(r)
logg.debug('account period {}'.format(account_period_data))
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
actual_period = c.parse_actual_period(r)
logg.debug('period {}'.format(actual_period))
redistribution = int((z / 2) * (self.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 * (self.tax_level / 1000000)))
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
spender_actual_balance = c.parse_balance_of(r)
logg.debug('rrr {} {}'.format(redistribution, spender_new_decayed_balance))
self.assertEqual(spender_actual_balance, spender_new_decayed_balance)
self.assertEqual(balance, 200000000)
# def test_debug_periods(self):
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
#
# o = c.actual_period(self.address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# pactual = c.parse_actual_period(r)
#
# o = c.period_start(self.address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# pstart = c.parse_actual_period(r)
#
# o = c.period_duration(self.address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# pduration = c.parse_actual_period(r)
#
# o = block_latest()
# blocknumber = self.rpc.do(o)
#
# logg.debug('actual {} start {} duration {} blocknumber {}'.format(pactual, pstart, pduration, blocknumber))
#
#
# # TODO: check receipt log outputs
# def test_redistribution_storage(self):
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# self.assertEqual(strip_0x(r), '000000000000000000000000f424000000000000000000000000000000000001')
#
# (tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000000)
# r = self.rpc.do(o)
#
# (tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1000000)
# r = self.rpc.do(o)
#
# external_address = to_checksum_address('0x' + os.urandom(20).hex())
#
# nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# (tx_hash, o) = c.transfer(self.address, self.accounts[2], external_address, 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.transfer(self.address, self.accounts[1], external_address, 999999)
# r = self.rpc.do(o)
#
# self.backend.time_travel(self.start_time + self.period_seconds + 1)
#
# o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# self.assertEqual(strip_0x(r), '000000000000000000000000f42400000000010000000000001e848000000001')
#
# o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# self.assertEqual(strip_0x(r), '000000000000000000000000f42400000000010000000000001e848000000001')
#
#
# 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], 1000000)
# r = self.rpc.do(o)
#
# o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# self.assertEqual(strip_0x(r), '000000000000000000000000ef4200000000000000000000002dc6c000000002')
#
#
# def test_redistribution_balance_on_zero_participants(self):
# supply = self.default_supply
#
# 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], supply)
# r = self.rpc.do(o)
#
# self.backend.time_travel(self.start_time + self.period_seconds + 1)
# (tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
# self.rpc.do(o)
# o = receipt(tx_hash)
# rcpt = self.rpc.do(o)
# self.assertEqual(rcpt['status'], 1)
#
# (tx_hash, o) = c.change_period(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.total_supply(self.address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# total_supply = c.parse_total_supply(r)
# sink_increment = int(total_supply * (self.tax_level / 1000000))
# self.assertEqual(supply, total_supply)
#
# for l in rcpt['logs']:
# if l['topics'][0] == '0xa0717e54e02bd9829db5e6e998aec0ae9de796b8d150a3cc46a92ab869697755': # event Decayed(uint256,uint256,uint256,uint256)
# period = int.from_bytes(bytes.fromhex(strip_0x(l['topics'][1])), 'big')
# self.assertEqual(period, 2)
# b = bytes.fromhex(strip_0x(l['data']))
# remainder = int.from_bytes(b, 'big')
# self.assertEqual(remainder, int((1000000 - self.tax_level) * (10 ** 32)))
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# sink_balance = c.parse_balance_of(r)
#
# self.assertEqual(sink_balance, int(sink_increment * 0.98))
# self.assertEqual(sink_balance, int(sink_increment * (1000000 - self.tax_level) / 1000000))
#
# o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance_of(r)
# self.assertEqual(balance, supply - sink_increment)
#
#
# def test_redistribution_two_of_ten(self):
# mint_amount = 100000000
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# z = 0
# for i in range(10):
# (tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i], mint_amount)
# self.rpc.do(o)
# z += mint_amount
#
# o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# initial_balance = c.parse_balance_of(r)
#
# spend_amount = 1000000
# external_address = to_checksum_address('0x' + os.urandom(20).hex())
#
# nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# (tx_hash, o) = c.transfer(self.address, self.accounts[1], external_address, spend_amount)
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# (tx_hash, o) = c.transfer(self.address, self.accounts[2], external_address, spend_amount)
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# # No cheating!
# nonce_oracle = RPCNonceOracle(self.accounts[3], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# (tx_hash, o) = c.transfer(self.address, self.accounts[3], self.accounts[3], spend_amount)
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# # No cheapskating!
# nonce_oracle = RPCNonceOracle(self.accounts[4], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
# (tx_hash, o) = c.transfer(self.address, self.accounts[4], external_address, spend_amount-1)
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
#
# self.backend.time_travel(self.start_time + self.period_seconds + 1)
#
# (tx_hash, o) = c.apply_demurrage(self.address, self.accounts[4])
# self.rpc.do(o)
#
# (tx_hash, o) = c.change_period(self.address, self.accounts[4])
# self.rpc.do(o)
#
# o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# bummer_balance = c.parse_balance_of(r)
#
# self.assertEqual(bummer_balance, mint_amount - (mint_amount * (self.tax_level / 1000000)))
# logg.debug('bal {} '.format(bummer_balance))
#
# o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# bummer_balance = c.parse_balance_of(r)
# spender_balance = mint_amount - spend_amount
# spender_decayed_balance = int(spender_balance - (spender_balance * (self.tax_level / 1000000)))
# self.assertEqual(bummer_balance, spender_decayed_balance)
# logg.debug('bal {} '.format(bummer_balance))
#
# (tx_hash, o) = c.apply_redistribution_on_account(self.address, self.accounts[4], self.accounts[1])
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# (tx_hash, o) = c.apply_redistribution_on_account(self.address, self.accounts[4], self.accounts[2])
# self.rpc.do(o)
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# o = c.redistributions(self.address, 0, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# redistribution_data = c.parse_redistributions(r)
# logg.debug('redist data {}'.format(redistribution_data))
#
# o = c.account_period(self.address, self.accounts[1], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# account_period_data = c.parse_account_period(r)
# logg.debug('account period {}'.format(account_period_data))
#
# o = c.actual_period(self.address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# actual_period = c.parse_actual_period(r)
# logg.debug('period {}'.format(actual_period))
#
# redistribution = int((z / 2) * (self.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 * (self.tax_level / 1000000)))
#
# o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
# r = self.rpc.do(o)
# spender_actual_balance = c.parse_balance_of(r)
# logg.debug('rrr {} {}'.format(redistribution, spender_new_decayed_balance))
#
# self.assertEqual(spender_actual_balance, spender_new_decayed_balance)
#
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,198 @@
# standard imports
import os
import unittest
import json
import logging
# 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,
)
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistribution(TestDemurrageDefault):
def test_redistribution_periods(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
supply = self.default_supply
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
self.rpc.do(o)
for i in range(1, 10):
logg.debug('execute time travel to period {}'.format(i))
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 = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.redistributions(self.address, i, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage = c.parse_to_redistribution_item(r)
o = c.redistributions(self.address, i-1, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_previous = c.parse_to_redistribution_item(r)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
logg.debug('testing sink {} mint {} adds up to supply {} with demurrage between {} and {}'.format(balance_sink, balance_minter, supply, demurrage_previous, demurrage))
self.assert_within_lower(balance_minter + balance_sink, supply, 0.001)
def test_redistribution_catchup_periods(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
supply = self.default_supply
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
self.rpc.do(o)
self.backend.time_travel(self.start_time + (self.period_seconds * 100))
balance_minter = None
balance_sink = None
real_supply = None
for i in range(1, 101):
(tx_hash, o) = c.change_period(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_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
real_supply = balance_sink + balance_minter
logg.info('period {} testing sink {} mint {} adds up to supply {} of original {} (delta {})'.format(i, balance_sink, balance_minter, real_supply, supply, supply - real_supply))
i = 100
o = c.redistributions(self.address, i, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage = c.parse_to_redistribution_item(r)
o = c.redistributions(self.address, i-1, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_previous = c.parse_to_redistribution_item(r)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
logg.debug('testing sink {} mint {} adds up to supply {} with demurrage between {} and {}'.format(balance_sink, balance_minter, real_supply, demurrage_previous, demurrage))
self.assert_within_lower(balance_minter + balance_sink, supply, 0.001)
# def test_redistribution_boundaries(self):
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
#
# demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
# supply = self.default_supply
#
# (tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
# self.rpc.do(o)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
# logg.debug('balance before {} supply {}'.format(balance, supply))
#
# self.backend.time_travel(self.start_time + self.period_seconds)
# (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.redistributions(self.address, 1, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# oo = c.to_redistribution_supply(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
# oo = c.to_redistribution_demurrage_modifier(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
#
# self.backend.time_travel(self.start_time + self.period_seconds * 2 + 1)
# (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.redistributions(self.address, 2, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# oo = c.to_redistribution_supply(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
# oo = c.to_redistribution_demurrage_modifier(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,199 @@
import os
import unittest
import json
import logging
# 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,
)
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest.base import TestDemurrageUnit
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistribution(TestDemurrageUnit):
# TODO: move to "pure" test file when getdistribution is implemented in all contracts
def test_distribution_direct(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
supply = self.default_supply
o = c.get_distribution(self.address, supply, demurrage, sender_address=self.accounts[0])
r = self.rpc.do(o)
distribution = c.parse_get_distribution(r)
expected_distribution = self.default_supply * (self.tax_level / 1000000)
self.assert_within_lower(distribution, expected_distribution, 1000)
def test_distribution_from_redistribution(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 100000)) * (10**28)
logg.debug('demurrage {}'.format(demurrage))
supply = self.default_supply
o = c.to_redistribution(self.address, 0, demurrage, supply, 2, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.get_distribution_from_redistribution(self.address, redistribution, self.accounts[0])
r = self.rpc.do(o)
distribution = c.parse_get_distribution(r)
expected_distribution = (self.default_supply * self.tax_level) / 100000
logg.debug('distribution {} supply {}'.format(distribution, self.default_supply))
self.assert_within_lower(distribution, expected_distribution, 1000)
def test_single_step_basic(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
mint_amount = 100000000
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], mint_amount)
self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
expected_balance = int(mint_amount - ((self.tax_level / 1000000) * mint_amount))
o = c.balance_of(self.address, ZERO_ADDRESS, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
logg.debug('balance {}'.format(balance))
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, expected_balance)
def test_single_step_multi(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
mint_amount = 100000000
for i in range(3):
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i+1], mint_amount)
self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
expected_balance = int(mint_amount - ((self.tax_level / 1000000) * mint_amount))
for i in range(3):
o = c.balance_of(self.address, self.accounts[i+1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, expected_balance)
def test_single_step_transfer(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
mint_amount = 100000000
half_mint_amount = int(mint_amount / 2)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], mint_amount)
self.rpc.do(o)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], mint_amount)
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.transfer(self.address, self.accounts[1], self.accounts[3], half_mint_amount)
self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.change_period(self.address, self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
# check that we have crossed into new period, this will throw if not
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
self.rpc.do(o)
demurrage_amount = int((self.tax_level / 1000000) * mint_amount)
expected_balance = mint_amount - demurrage_amount
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, expected_balance)
half_demurrage_amount = int((self.tax_level / 1000000) * half_mint_amount)
expected_balance = half_mint_amount - half_demurrage_amount
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, expected_balance)
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, expected_balance)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
supply = c.parse_total_supply(r)
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)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage = c.parse_to_redistribution_item(r)
logg.debug('\nrediistribution {}\ndemurrage {}\nsupply {}'.format(redistribution, demurrage, supply))
expected_balance = int(supply * (self.tax_level / 1000000))
expected_balance_tolerance = 1
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, expected_balance, 1000)
if __name__ == '__main__':
unittest.main()

View File

@@ -16,7 +16,7 @@ import eth_tester
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageDefault
from erc20_demurrage_token.unittest.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()

View File

@@ -18,7 +18,7 @@ from hexathon import (
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageSingle
from erc20_demurrage_token.unittest.base import TestDemurrageSingle
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -28,6 +28,7 @@ testdir = os.path.dirname(__file__)
class TestRedistributionSingle(TestDemurrageSingle):
def test_single_even_if_multiple(self):
mint_amount = 100000000
@@ -63,22 +64,23 @@ class TestRedistributionSingle(TestDemurrageSingle):
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
tax_modifier = (1 - (self.tax_level / 1000000)) ** 10
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, mint_amount - (mint_amount * (self.tax_level / 1000000)))
balance = c.parse_balance(r)
self.assertEqual(balance, int(mint_amount * tax_modifier))
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
balance = c.parse_balance(r)
base_amount = mint_amount - int(mint_amount * 0.1)
self.assertEqual(balance, (base_amount - (base_amount * (self.tax_level / 1000000))))
self.assertEqual(balance, int(base_amount * tax_modifier)) #(base_amount - (base_amount * (self.tax_level / 1000000))))
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
balance = c.parse_balance(r)
base_amount = mint_amount - int(mint_amount * 0.2)
self.assertEqual(balance, (base_amount - (base_amount * (self.tax_level / 1000000))))
self.assertEqual(balance, int(base_amount * tax_modifier)) #(base_amount - (base_amount * (self.tax_level / 1000000))))
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
@@ -86,9 +88,9 @@ class TestRedistributionSingle(TestDemurrageSingle):
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
base_amount = new_supply * (self.tax_level / 1000000)
self.assertEqual(balance, base_amount - (base_amount * (self.tax_level / 1000000)))
balance = c.parse_balance(r)
expected_balance = new_supply - (new_supply * tax_modifier)
self.assert_within_lower(balance, expected_balance, 1)
if __name__ == '__main__':

View File

@@ -30,7 +30,9 @@ contract DemurrageTokenMultiCap {
uint128 public demurrageAmount;
// Cached demurrage period; the period for which demurrageAmount was calculated
uint128 public demurragePeriod;
//uint128 public demurragePeriod;
// Cached demurrage timestamp; the timestamp for which demurrageAmount was last calculated
uint256 public demurrageTimestamp;
// Implements EIP172
address public owner;
@@ -59,6 +61,9 @@ contract DemurrageTokenMultiCap {
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
// demurrage decimal width; 38 places
uint256 public immutable resolutionFactor = ppmDivider * 1000000;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
@@ -112,10 +117,11 @@ contract DemurrageTokenMultiCap {
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
demurrageTimestamp = block.timestamp;
periodStart = demurrageTimestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
//demurragePeriod = 1;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
redistributions.push(initialRedistribution);
@@ -148,7 +154,8 @@ contract DemurrageTokenMultiCap {
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
//periodCount = actualPeriod() - demurragePeriod;
periodCount = getMinutesDelta(demurrageTimestamp);
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
@@ -211,7 +218,7 @@ contract DemurrageTokenMultiCap {
require(_amount + totalSupply <= supplyCap);
changePeriod();
baseAmount = _amount;
baseAmount = toBaseAmount(_amount);
totalSupply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
@@ -246,7 +253,7 @@ contract DemurrageTokenMultiCap {
return (uint256(redistribution) & maskRedistributionParticipants) >> shiftRedistributionParticipants;
}
// Serializes the number of participants part of the redistribution word
// Serializes the demurrage modifier part of the redistribution word
function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionDemurrage) >> shiftRedistributionDemurrage;
}
@@ -374,22 +381,30 @@ contract DemurrageTokenMultiCap {
}
// Calculate the time delta in whole minutes passed between given timestamp and current timestamp
function getMinutesDelta(uint256 _lastTimestamp) public view returns (uint256) {
return (block.timestamp - _lastTimestamp) / 60;
}
// Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
function applyDemurrage() public returns (bool) {
uint128 epochPeriodCount;
uint128 periodCount;
//uint128 epochPeriodCount;
uint256 periodCount;
uint256 lastDemurrageAmount;
uint256 newDemurrageAmount;
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
//epochPeriodCount = actualPeriod();
//periodCount = epochPeriodCount - demurragePeriod;
periodCount = getMinutesDelta(demurrageTimestamp);
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
//demurragePeriod = epochPeriodCount;
demurrageTimestamp = demurrageTimestamp + (periodCount * 60);
//emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
emit Decayed(demurrageTimestamp, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
@@ -417,6 +432,8 @@ contract DemurrageTokenMultiCap {
uint256 periodTimestamp;
uint256 nextPeriod;
applyDemurrage();
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
return false;
@@ -426,7 +443,7 @@ contract DemurrageTokenMultiCap {
nextPeriod = currentPeriod + 1;
periodTimestamp = getPeriodTimeDelta(currentPeriod);
applyDemurrage();
//applyDemurrage();
currentDemurrageAmount = demurrageAmount;
demurrageCounts = demurrageCycles(periodTimestamp);

View File

@@ -56,6 +56,9 @@ contract DemurrageTokenMultiNocap {
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
// demurrage decimal width; 38 places
uint256 public immutable resolutionFactor = ppmDivider * 1000000;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;

View File

@@ -2,35 +2,28 @@ pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleNocap {
contract DemurrageTokenSingleCap {
// Redistribution bit field, with associated shifts and masks
// (Uses sub-byte boundaries)
bytes32[] public redistributions; // uint1(isFractional) | uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
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 shiftRedistributionParticipants = 104;
uint256 constant maskRedistributionParticipants = 0x00000000000000000000000000000fffffffff00000000000000000000000000; // ((1 << 36) - 1) << 104
uint8 constant shiftRedistributionDemurrage = 140;
uint256 constant maskRedistributionDemurrage = 0x000000000000000000000000fffff00000000000000000000000000000000000; // ((1 << 20) - 1) << 140
uint8 constant shiftRedistributionIsFractional = 255;
uint256 constant maskRedistributionIsFractional = 0x8000000000000000000000000000000000000000000000000000000000000000; // 1 << 255
uint8 constant shiftRedistributionDemurrage = 104;
uint256 constant maskRedistributionDemurrage = 0x0000000000ffffffffffffffffffffffffffff00000000000000000000000000; // ((1 << 20) - 1) << 140
// Account bit field, with associated shifts and masks
// Mirrors structure of redistributions for consistency
mapping (address => bytes32) account; // uint152(unused) | uint32(period) | uint72(value)
uint8 constant shiftAccountValue = 0;
uint256 constant maskAccountValue = 0x0000000000000000000000000000000000000000000000ffffffffffffffffff; // (1 << 72) - 1
uint8 constant shiftAccountPeriod = 72;
uint256 constant maskAccountPeriod = 0x00000000000000000000000000000000000000ffffffff000000000000000000; // ((1 << 32) - 1) << 72
// Account balances
mapping (address => uint256) account;
// Cached demurrage amount, ppm with 38 digit resolution
uint128 public demurrageAmount;
// Cached demurrage period; the period for which demurrageAmount was calculated
uint128 public demurragePeriod;
//uint128 public demurragePeriod;
// Cached demurrage timestamp; the timestamp for which demurrageAmount was last calculated
uint256 public demurrageTimestamp;
// Implements EIP172
address public owner;
@@ -57,7 +50,13 @@ contract DemurrageTokenSingleNocap {
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
uint256 constant nanoDivider = 100000000000000000000000000; // now nanodivider, 6 zeros less
// remaining decimal positions of nanoDivider to reach 38, equals precision in growth and decay
uint256 constant growthResolutionFactor = 1000000000000;
// demurrage decimal width; 38 places
uint256 public immutable resolutionFactor = nanoDivider * growthResolutionFactor;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
@@ -67,7 +66,7 @@ contract DemurrageTokenSingleNocap {
// Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
@@ -101,7 +100,7 @@ contract DemurrageTokenSingleNocap {
// EIP173
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // EIP173
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress, uint256 _supplyCap) public {
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint128 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress, uint256 _supplyCap) public {
// ACL setup
owner = msg.sender;
minter[owner] = true;
@@ -112,12 +111,15 @@ contract DemurrageTokenSingleNocap {
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
demurrageTimestamp = block.timestamp;
periodStart = demurrageTimestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
//demurrageAmount = 100000000000000000000000000000000000000 - _taxLevelMinute; // Represents 38 decimal places, same as resolutionFactor
//demurrageAmount = 100000000000000000000000000000000000000;
demurrageAmount = 10000000000000000000000000000;
//demurragePeriod = 1;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
bytes32 initialRedistribution = toRedistribution(0, demurrageAmount, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
@@ -126,6 +128,12 @@ contract DemurrageTokenSingleNocap {
minimumParticipantSpend = 10 ** uint256(_decimals);
}
// Change sink address for redistribution
function setSinkAddress(address _sinkAddress) public {
require(msg.sender == owner);
sinkAddress = _sinkAddress;
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(msg.sender == owner);
@@ -148,16 +156,17 @@ contract DemurrageTokenSingleNocap {
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
//periodCount = actualPeriod() - demurragePeriod;
periodCount = getMinutesDelta(demurrageTimestamp);
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
currentDemurragedAmount = uint128(decayBy(demurrageAmount * 10000000000, periodCount));
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
return (baseBalance * currentDemurragedAmount) / (nanoDivider * 1000000000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return uint256(account[_account]) & maskAccountValue;
return account[_account];
}
/// Increases base balance for a single account
@@ -173,11 +182,7 @@ contract DemurrageTokenSingleNocap {
}
oldBalance = baseBalanceOf(_account);
newBalance = oldBalance + _delta;
require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value
workAccount &= (~maskAccountValue);
workAccount |= (newBalance & maskAccountValue);
account[_account] = bytes32(workAccount);
account[_account] = oldBalance + _delta;
return true;
}
@@ -195,10 +200,7 @@ contract DemurrageTokenSingleNocap {
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
newBalance = oldBalance - _delta;
workAccount &= (~maskAccountValue);
workAccount |= (newBalance & maskAccountValue);
account[_account] = bytes32(workAccount);
account[_account] = oldBalance - _delta;
return true;
}
@@ -207,11 +209,11 @@ contract DemurrageTokenSingleNocap {
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(minter[msg.sender]);
require(_amount + totalSupply <= supplyCap);
require(minter[msg.sender], 'ERR_ACCESS');
require(_amount + totalSupply <= supplyCap, 'ERR_CAP');
changePeriod();
baseAmount = _amount;
baseAmount = toBaseAmount(_amount);
totalSupply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
@@ -220,12 +222,11 @@ contract DemurrageTokenSingleNocap {
}
// Deserializes the redistribution word
// 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) {
// 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 |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution;
@@ -241,11 +242,6 @@ contract DemurrageTokenSingleNocap {
return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionParticipants) >> shiftRedistributionParticipants;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionDemurrage) >> shiftRedistributionDemurrage;
@@ -256,30 +252,16 @@ contract DemurrageTokenSingleNocap {
return redistributions.length;
}
// Add number of participants for the current redistribution period by one
function incrementRedistributionParticipants() private returns (bool) {
bytes32 currentRedistribution;
uint256 tmpRedistribution;
uint256 participants;
currentRedistribution = redistributions[redistributions.length-1];
participants = toRedistributionParticipants(currentRedistribution) + 1;
tmpRedistribution = uint256(currentRedistribution);
tmpRedistribution &= (~maskRedistributionParticipants);
tmpRedistribution |= ((participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistributions[redistributions.length-1] = bytes32(tmpRedistribution);
return true;
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
uint256 currentRedistribution;
uint256 grownSupply;
//grownSupply = growBy(totalSupply, 1);
grownSupply = totalSupply;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (totalSupply << shiftRedistributionValue);
currentRedistribution |= (grownSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true;
@@ -303,92 +285,64 @@ contract DemurrageTokenSingleNocap {
return lastRedistribution;
}
// Deserialize the pemurrage period for the given account is participating in
function accountPeriod(address _account) public view returns (uint256) {
return (uint256(account[_account]) & maskAccountPeriod) >> shiftAccountPeriod;
function getDistribution(uint256 _supply, uint256 _demurrageAmount) public view returns (uint256) {
uint256 difference;
difference = _supply * (resolutionFactor - (_demurrageAmount * 10000000000)); //(nanoDivider - ((resolutionFactor - _demurrageAmount) / nanoDivider));
return difference / resolutionFactor;
}
// 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] &= bytes32(~maskAccountPeriod);
account[_account] |= bytes32((_period << shiftAccountPeriod) & maskAccountPeriod);
incrementRedistributionParticipants();
return true;
}
function getDistributionFromRedistribution(bytes32 _redistribution) public returns (uint256) {
uint256 redistributionSupply;
uint256 redistributionDemurrage;
// Determine whether the unit number is rounded down, rounded up or evenly divides.
// Returns 0 if evenly distributed, or the remainder as a positive number
// A _numParts value 0 will be interpreted as the value 1
function remainder(uint256 _numParts, uint256 _sumWhole) public pure returns (uint256) {
uint256 unit;
uint256 truncatedResult;
if (_numParts == 0) { // no division by zero please
revert('ERR_NUMPARTS_ZERO');
}
require(_numParts < _sumWhole); // At least you are never LESS than the sum of your parts. Think about that.
unit = _sumWhole / _numParts;
truncatedResult = unit * _numParts;
return _sumWhole - truncatedResult;
redistributionSupply = toRedistributionSupply(_redistribution);
redistributionDemurrage = toRedistributionDemurrageModifier(_redistribution);
return getDistribution(redistributionSupply, redistributionDemurrage);
}
// Returns the amount 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] &= bytes32(~maskRedistributionParticipants); // just to be safe, zero out all participant count data, in this case there will be only one
redistributions[redistributionPeriod-1] |= bytes32(maskRedistributionIsFractional | (1 << shiftRedistributionParticipants));
}
increaseBaseBalance(sinkAddress, unit / ppmDivider);
unit = getDistributionFromRedistribution(_redistribution);
increaseBaseBalance(sinkAddress, toBaseAmount(unit));
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) {
uint256 periodSupply;
if (_remainder == 0) {
return false;
}
// TODO: is this needed?
redistributions[_period-1] |= bytes32(maskRedistributionIsFractional);
periodSupply = toRedistributionSupply(redistributions[_period-1]);
increaseBaseBalance(sinkAddress, periodSupply - _remainder);
return true;
// Calculate the time delta in whole minutes passed between given timestamp and current timestamp
function getMinutesDelta(uint256 _lastTimestamp) public view returns (uint256) {
return (block.timestamp - _lastTimestamp) / 60;
}
// Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
function applyDemurrage() public returns (bool) {
uint128 epochPeriodCount;
uint128 periodCount;
uint256 lastDemurrageAmount;
uint256 newDemurrageAmount;
return applyDemurrageLimited(0);
}
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
function applyDemurrageLimited(uint256 _rounds) public returns (bool) {
//uint128 epochPeriodCount;
uint256 periodCount;
uint256 lastDemurrageAmount;
//epochPeriodCount = actualPeriod();
//periodCount = epochPeriodCount - demurragePeriod;
periodCount = getMinutesDelta(demurrageTimestamp);
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
// safety limit for exponential calculation to ensure that we can always
// execute this code no matter how much time passes.
if (_rounds > 0 && _rounds < periodCount) {
periodCount = _rounds;
}
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
//demurragePeriod = epochPeriodCount;
demurrageTimestamp = demurrageTimestamp + (periodCount * 60);
emit Decayed(demurrageTimestamp, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
@@ -407,14 +361,13 @@ contract DemurrageTokenSingleNocap {
bytes32 currentRedistribution;
bytes32 nextRedistribution;
uint256 currentPeriod;
uint256 currentParticipants;
uint256 currentRemainder;
uint256 currentDemurrageAmount;
uint256 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 periodTimestamp;
uint256 nextPeriod;
applyDemurrage();
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
return false;
@@ -424,26 +377,19 @@ contract DemurrageTokenSingleNocap {
nextPeriod = currentPeriod + 1;
periodTimestamp = getPeriodTimeDelta(currentPeriod);
applyDemurrage();
currentDemurrageAmount = demurrageAmount;
demurrageCounts = demurrageCycles(periodTimestamp);
if (demurrageCounts > 0) {
nextRedistributionDemurrage = growBy(currentDemurrageAmount, demurrageCounts) / ppmDivider;
nextRedistributionDemurrage = growBy(currentDemurrageAmount, demurrageCounts);
} else {
nextRedistributionDemurrage = currentDemurrageAmount / ppmDivider;
nextRedistributionDemurrage = currentDemurrageAmount;
}
nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply, nextPeriod);
redistributions.push(nextRedistribution);
//currentParticipants = toRedistributionParticipants(currentRedistribution);
//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);
//}
applyDefaultRedistribution(nextRedistribution);
emit Period(nextPeriod);
return true;
}
@@ -453,68 +399,32 @@ contract DemurrageTokenSingleNocap {
uint256 valueFactor;
uint256 truncatedTaxLevel;
valueFactor = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider;
valueFactor = growthResolutionFactor;
truncatedTaxLevel = taxLevel / nanoDivider;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor + ((valueFactor * truncatedTaxLevel) / 1000000);
valueFactor = valueFactor + ((valueFactor * truncatedTaxLevel) / growthResolutionFactor);
}
return (valueFactor * _value) / 1000000;
return (valueFactor * _value) / growthResolutionFactor;
}
// Calculate a value reduced by demurrage by the given period
// TODO: higher precision if possible
function decayBy(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor;
uint256 truncatedTaxLevel;
valueFactor = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider;
valueFactor = growthResolutionFactor;
truncatedTaxLevel = taxLevel / nanoDivider;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000);
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / growthResolutionFactor);
}
return (valueFactor * _value) / 1000000;
}
// If the given account is participating in a period and that period has been crossed
// THEN increase the base value of the account with its share of the value reduction of the period
function applyRedistributionOnAccount(address _account) public returns (bool) {
// bytes32 periodRedistribution;
// uint256 supply;
// uint256 participants;
// uint256 baseValue;
// uint256 value;
uint256 period;
// uint256 demurrage;
//
period = accountPeriod(_account);
if (period == 0 || period >= actualPeriod()) {
return false;
}
// periodRedistribution = redistributions[period-1];
// participants = toRedistributionParticipants(periodRedistribution);
// if (participants == 0) {
// return false;
// }
//
// supply = toRedistributionSupply(periodRedistribution);
// demurrage = toRedistributionDemurrageModifier(periodRedistribution);
// baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider;
// value = (baseValue * demurrage) / 1000000;
//
// // zero out period for the account
account[_account] &= bytes32(~maskAccountPeriod);
// increaseBaseBalance(_account, value);
//
// emit Redistribution(_account, period, value);
return true;
return (valueFactor * _value) / growthResolutionFactor;
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
//return (_value * ppmDivider * 1000000) / toDemurrageAmount(demurrageModifier);
return (_value * ppmDivider * 1000000) / demurrageAmount;
return (_value * resolutionFactor) / (demurrageAmount * 10000000000);
}
// Implements ERC20, triggers tax and/or redistribution
@@ -522,7 +432,6 @@ contract DemurrageTokenSingleNocap {
uint256 baseValue;
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
@@ -536,7 +445,6 @@ contract DemurrageTokenSingleNocap {
bool result;
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue);
@@ -544,14 +452,12 @@ contract DemurrageTokenSingleNocap {
return result;
}
// Implements ERC20, triggers tax and/or redistribution
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
@@ -568,10 +474,7 @@ contract DemurrageTokenSingleNocap {
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
period = actualPeriod();
if (_value >= minimumParticipantSpend && accountPeriod(_from) != period && _from != _to) {
registerAccountPeriod(_from, period);
}
//period = actualPeriod();
return true;
}

View File

@@ -1,36 +1,24 @@
pragma solidity > 0.6.11;
pragma solidity >= 0.8.0;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleCap {
contract DemurrageTokenSingleNocap {
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; // uint1(isFractional) | uint95(unused) | uint20(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 shiftRedistributionParticipants = 104;
uint256 constant maskRedistributionParticipants = 0x00000000000000000000000000000fffffffff00000000000000000000000000; // ((1 << 36) - 1) << 104
uint8 constant shiftRedistributionDemurrage = 140;
uint256 constant maskRedistributionDemurrage = 0x000000000000000000000000fffff00000000000000000000000000000000000; // ((1 << 20) - 1) << 140
uint8 constant shiftRedistributionIsFractional = 255;
uint256 constant maskRedistributionIsFractional = 0x8000000000000000000000000000000000000000000000000000000000000000; // 1 << 255
// Account bit field, with associated shifts and masks
// Mirrors structure of redistributions for consistency
mapping (address => bytes32) account; // uint152(unused) | uint32(period) | uint72(value)
uint8 constant shiftAccountValue = 0;
uint256 constant maskAccountValue = 0x0000000000000000000000000000000000000000000000ffffffffffffffffff; // (1 << 72) - 1
uint8 constant shiftAccountPeriod = 72;
uint256 constant maskAccountPeriod = 0x00000000000000000000000000000000000000ffffffff000000000000000000; // ((1 << 32) - 1) << 72
// Account balances
mapping (address => uint256) account;
// Cached demurrage amount, ppm with 38 digit resolution
uint128 public demurrageAmount;
// Cached demurrage period; the period for which demurrageAmount was calculated
uint128 public demurragePeriod;
// Cached demurrage timestamp; the timestamp for which demurrageAmount was last calculated
uint256 public demurrageTimestamp;
// Implements EIP172
address public owner;
@@ -47,14 +35,27 @@ contract DemurrageTokenSingleNocap {
uint256 public decimals;
// Implements ERC20
uint256 public totalSupply;
//uint256 public totalSupply;
uint256 supply;
// Minimum amount of (demurraged) tokens an account must spend to participate in redistribution for a particular period
uint256 public minimumParticipantSpend;
// Last executed period
uint256 public lastPeriod;
// Last sink redistribution amount
uint256 public totalSink;
// Value of burnt tokens (burnt tokens do not decay)
uint256 public burned;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
uint256 constant nanoDivider = 100000000000000000000000000; // now nanodivider, 6 zeros less
// remaining decimal positions of nanoDivider to reach 38, equals precision in growth and decay
uint256 constant growthResolutionFactor = 1000000000000;
// demurrage decimal width; 38 places
uint256 public immutable resolutionFactor = nanoDivider * growthResolutionFactor;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
@@ -64,7 +65,7 @@ contract DemurrageTokenSingleNocap {
// Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
@@ -72,7 +73,7 @@ contract DemurrageTokenSingleNocap {
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
// Address to send unallocated redistribution tokens
address sinkAddress;
address public sinkAddress;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value);
@@ -95,10 +96,15 @@ contract DemurrageTokenSingleNocap {
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
// Emitted when tokens are burned
event Burn(address indexed _burner, uint256 _value);
// EIP173
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // EIP173
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress) public {
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;
@@ -109,17 +115,23 @@ contract DemurrageTokenSingleNocap {
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
demurrageTimestamp = block.timestamp;
periodStart = demurrageTimestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
demurrageAmount = uint128(nanoDivider) * 100;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
initialRedistribution = toRedistribution(0, demurrageAmount, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
sinkAddress = _defaultSinkAddress;
minimumParticipantSpend = 10 ** uint256(_decimals);
}
// Change sink address for redistribution
function setSinkAddress(address _sinkAddress) public {
require(msg.sender == owner);
sinkAddress = _sinkAddress;
}
// Given address will be allowed to call the mintTo() function
@@ -144,16 +156,16 @@ contract DemurrageTokenSingleNocap {
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
periodCount = getMinutesDelta(demurrageTimestamp);
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
currentDemurragedAmount = uint128(decayBy(demurrageAmount * 10000000000, periodCount));
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
return (baseBalance * currentDemurragedAmount) / (nanoDivider * 1000000000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return uint256(account[_account]) & maskAccountValue;
return account[_account];
}
/// Increases base balance for a single account
@@ -169,11 +181,7 @@ contract DemurrageTokenSingleNocap {
}
oldBalance = baseBalanceOf(_account);
newBalance = oldBalance + _delta;
require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value
workAccount &= (~maskAccountValue);
workAccount |= (newBalance & maskAccountValue);
account[_account] = bytes32(workAccount);
account[_account] = oldBalance + _delta;
return true;
}
@@ -191,10 +199,7 @@ contract DemurrageTokenSingleNocap {
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
newBalance = oldBalance - _delta;
workAccount &= (~maskAccountValue);
workAccount |= (newBalance & maskAccountValue);
account[_account] = bytes32(workAccount);
account[_account] = oldBalance - _delta;
return true;
}
@@ -203,11 +208,11 @@ contract DemurrageTokenSingleNocap {
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(minter[msg.sender]);
require(minter[msg.sender], 'ERR_ACCESS');
changePeriod();
baseAmount = _amount;
totalSupply += _amount;
baseAmount = toBaseAmount(_amount);
supply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
saveRedistributionSupply();
@@ -215,68 +220,47 @@ contract DemurrageTokenSingleNocap {
}
// Deserializes the redistribution word
// 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;
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) public pure returns(redistributionItem memory) {
redistributionItem memory redistribution;
redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
redistribution.period = uint32(_period);
redistribution.value = uint72(_value);
redistribution.demurrage = uint104(_demurrageModifierPpm);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & maskRedistributionPeriod;
function toRedistributionPeriod(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.period);
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
function toRedistributionSupply(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.value);
}
// Serializes the number of participants part of the redistribution word
function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionParticipants) >> shiftRedistributionParticipants;
function toRedistributionDemurrageModifier(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.demurrage);
}
// Serializes the number of participants part of the redistribution word
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;
}
// Add number of participants for the current redistribution period by one
function incrementRedistributionParticipants() private returns (bool) {
bytes32 currentRedistribution;
uint256 tmpRedistribution;
uint256 participants;
currentRedistribution = redistributions[redistributions.length-1];
participants = toRedistributionParticipants(currentRedistribution) + 1;
tmpRedistribution = uint256(currentRedistribution);
tmpRedistribution &= (~maskRedistributionParticipants);
tmpRedistribution |= ((participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistributions[redistributions.length-1] = bytes32(tmpRedistribution);
return true;
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
uint256 currentRedistribution;
redistributionItem memory currentRedistribution;
uint256 grownSupply;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (totalSupply << shiftRedistributionValue);
grownSupply = totalSupply();
currentRedistribution = redistributions[redistributions.length-1];
currentRedistribution.value = uint72(grownSupply);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
redistributions[redistributions.length-1] = currentRedistribution;
return true;
}
@@ -285,105 +269,79 @@ contract DemurrageTokenSingleNocap {
return uint128((block.timestamp - periodStart) / periodDuration + 1);
}
// Add an entered demurrage period to the redistribution array
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
// Retrieve next redistribution if the period threshold has been crossed
function checkPeriod() private view returns (redistributionItem memory) {
redistributionItem memory lastRedistribution;
redistributionItem memory emptyRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[redistributions.length-1];
lastRedistribution = redistributions[lastPeriod];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return bytes32(0x00);
return emptyRedistribution;
}
return lastRedistribution;
}
// Deserialize the pemurrage period for the given account is participating in
function accountPeriod(address _account) public view returns (uint256) {
return (uint256(account[_account]) & maskAccountPeriod) >> shiftAccountPeriod;
function getDistribution(uint256 _supply, uint256 _demurrageAmount) public view returns (uint256) {
uint256 difference;
difference = _supply * (resolutionFactor - (_demurrageAmount * 10000000000));
return difference / resolutionFactor;
}
// 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] &= bytes32(~maskAccountPeriod);
account[_account] |= bytes32((_period << shiftAccountPeriod) & maskAccountPeriod);
incrementRedistributionParticipants();
return true;
}
function getDistributionFromRedistribution(redistributionItem memory _redistribution) public returns (uint256) {
uint256 redistributionSupply;
uint256 redistributionDemurrage;
// Determine whether the unit number is rounded down, rounded up or evenly divides.
// Returns 0 if evenly distributed, or the remainder as a positive number
// A _numParts value 0 will be interpreted as the value 1
function remainder(uint256 _numParts, uint256 _sumWhole) public pure returns (uint256) {
uint256 unit;
uint256 truncatedResult;
if (_numParts == 0) { // no division by zero please
revert('ERR_NUMPARTS_ZERO');
}
require(_numParts < _sumWhole); // At least you are never LESS than the sum of your parts. Think about that.
unit = _sumWhole / _numParts;
truncatedResult = unit * _numParts;
return _sumWhole - truncatedResult;
redistributionSupply = toRedistributionSupply(_redistribution);
redistributionDemurrage = toRedistributionDemurrageModifier(_redistribution);
return getDistribution(redistributionSupply, redistributionDemurrage);
}
// Returns the amount sent to the sink address
function applyDefaultRedistribution(bytes32 _redistribution) private returns (uint256) {
uint256 redistributionSupply;
uint256 redistributionPeriod;
function applyDefaultRedistribution(redistributionItem memory _redistribution) private returns (uint256) {
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] &= bytes32(~maskRedistributionParticipants); // just to be safe, zero out all participant count data, in this case there will be only one
redistributions[redistributionPeriod-1] |= bytes32(maskRedistributionIsFractional | (1 << shiftRedistributionParticipants));
}
increaseBaseBalance(sinkAddress, unit / ppmDivider);
uint256 baseUnit;
unit = getDistributionFromRedistribution(_redistribution);
baseUnit = toBaseAmount(unit) - totalSink;
increaseBaseBalance(sinkAddress, baseUnit);
lastPeriod += 1;
totalSink += baseUnit;
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) {
uint256 periodSupply;
if (_remainder == 0) {
return false;
}
// TODO: is this needed?
redistributions[_period-1] |= bytes32(maskRedistributionIsFractional);
periodSupply = toRedistributionSupply(redistributions[_period-1]);
increaseBaseBalance(sinkAddress, periodSupply - _remainder);
return true;
// Calculate the time delta in whole minutes passed between given timestamp and current timestamp
function getMinutesDelta(uint256 _lastTimestamp) public view returns (uint256) {
return (block.timestamp - _lastTimestamp) / 60;
}
// Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
function applyDemurrage() public returns (bool) {
uint128 epochPeriodCount;
uint128 periodCount;
uint256 lastDemurrageAmount;
uint256 newDemurrageAmount;
return applyDemurrageLimited(0);
}
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
function applyDemurrageLimited(uint256 _rounds) public returns (bool) {
uint256 periodCount;
uint256 lastDemurrageAmount;
periodCount = getMinutesDelta(demurrageTimestamp);
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
// safety limit for exponential calculation to ensure that we can always
// execute this code no matter how much time passes.
if (_rounds > 0 && _rounds < periodCount) {
periodCount = _rounds;
}
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
//demurragePeriod = epochPeriodCount;
demurrageTimestamp = demurrageTimestamp + (periodCount * 60);
emit Decayed(demurrageTimestamp, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
@@ -397,141 +355,135 @@ contract DemurrageTokenSingleNocap {
return (block.timestamp - _target) / 60;
}
function isEmptyRedistribution(redistributionItem memory _redistribution) public pure returns(bool) {
if (_redistribution.period > 0) {
return false;
}
if (_redistribution.value > 0) {
return false;
}
if (_redistribution.demurrage > 0) {
return false;
}
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) {
bytes32 currentRedistribution;
bytes32 nextRedistribution;
redistributionItem memory currentRedistribution;
redistributionItem memory nextRedistribution;
redistributionItem memory lastRedistribution;
uint256 currentPeriod;
uint256 currentParticipants;
uint256 currentRemainder;
uint256 currentDemurrageAmount;
uint256 lastDemurrageAmount;
uint256 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 periodTimestamp;
uint256 nextPeriod;
applyDemurrage();
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
if (isEmptyRedistribution(currentRedistribution)) {
return false;
}
// calculate the decay from previous redistributino
lastRedistribution = redistributions[lastPeriod];
currentPeriod = toRedistributionPeriod(currentRedistribution);
nextPeriod = currentPeriod + 1;
periodTimestamp = getPeriodTimeDelta(currentPeriod);
applyDemurrage();
currentDemurrageAmount = demurrageAmount;
demurrageCounts = demurrageCycles(periodTimestamp);
if (demurrageCounts > 0) {
nextRedistributionDemurrage = growBy(currentDemurrageAmount, demurrageCounts) / ppmDivider;
} else {
nextRedistributionDemurrage = currentDemurrageAmount / ppmDivider;
}
nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply, nextPeriod);
lastDemurrageAmount = toRedistributionDemurrageModifier(lastRedistribution);
demurrageCounts = periodDuration / 60;
nextRedistributionDemurrage = decayBy(lastDemurrageAmount, demurrageCounts);
nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply(), nextPeriod);
redistributions.push(nextRedistribution);
//currentParticipants = toRedistributionParticipants(currentRedistribution);
//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);
//}
applyDefaultRedistribution(nextRedistribution);
emit Period(nextPeriod);
return true;
}
// 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 = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor + ((valueFactor * truncatedTaxLevel) / 1000000);
}
return (valueFactor * _value) / 1000000;
}
// 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
// TODO: higher precision if possible
function decayBy(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor;
uint256 truncatedTaxLevel;
valueFactor = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider;
valueFactor = growthResolutionFactor;
truncatedTaxLevel = taxLevel / nanoDivider;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000);
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / growthResolutionFactor);
}
return (valueFactor * _value) / 1000000;
}
// If the given account is participating in a period and that period has been crossed
// THEN increase the base value of the account with its share of the value reduction of the period
function applyRedistributionOnAccount(address _account) public returns (bool) {
// bytes32 periodRedistribution;
// uint256 supply;
// uint256 participants;
// uint256 baseValue;
// uint256 value;
uint256 period;
// uint256 demurrage;
//
period = accountPeriod(_account);
if (period == 0 || period >= actualPeriod()) {
return false;
}
// periodRedistribution = redistributions[period-1];
// participants = toRedistributionParticipants(periodRedistribution);
// if (participants == 0) {
// return false;
// }
//
// supply = toRedistributionSupply(periodRedistribution);
// demurrage = toRedistributionDemurrageModifier(periodRedistribution);
// baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider;
// value = (baseValue * demurrage) / 1000000;
//
// // zero out period for the account
account[_account] &= bytes32(~maskAccountPeriod);
// increaseBaseBalance(_account, value);
//
// emit Redistribution(_account, period, value);
return true;
return (valueFactor * _value) / growthResolutionFactor;
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
//return (_value * ppmDivider * 1000000) / toDemurrageAmount(demurrageModifier);
return (_value * ppmDivider * 1000000) / demurrageAmount;
return (_value * resolutionFactor) / (demurrageAmount * 10000000000);
}
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
if (allowance[msg.sender][_spender] > 0) {
require(_value == 0, 'ZERO_FIRST');
}
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
allowance[msg.sender][_spender] = baseValue;
emit Approval(msg.sender, _spender, _value);
return true;
}
// Reduce allowance by amount
function decreaseAllowance(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
baseValue = toBaseAmount(_value);
require(allowance[msg.sender][_spender] >= baseValue);
changePeriod();
allowance[msg.sender][_spender] -= baseValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
// Increase allowance by amount
function increaseAllowance(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
changePeriod();
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
// Implements ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue);
@@ -539,19 +491,19 @@ contract DemurrageTokenSingleNocap {
return result;
}
// Implements ERC20, triggers tax and/or redistribution
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
changePeriod();
//applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
allowance[_from][msg.sender] -= baseValue;
result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result;
}
@@ -563,10 +515,6 @@ contract DemurrageTokenSingleNocap {
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
period = actualPeriod();
if (_value >= minimumParticipantSpend && accountPeriod(_from) != period && _from != _to) {
registerAccountPeriod(_from, period);
}
return true;
}
@@ -587,6 +535,29 @@ contract DemurrageTokenSingleNocap {
emit OwnershipTransferred(oldOwner, owner);
}
// Explicitly and irretrievably burn tokens
// Only token minters can burn tokens
function burn(uint256 _value) public {
require(minter[msg.sender]);
require(_value <= account[msg.sender]);
uint256 _delta = toBaseAmount(_value);
applyDemurrage();
decreaseBaseBalance(msg.sender, _delta);
burned += _value;
emit Burn(msg.sender, _value);
}
// Implements ERC20
function totalSupply() public view returns (uint256) {
return supply - burned;
}
// Return total number of burned tokens
function totalBurned() public view returns (uint256) {
return burned;
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20

View File

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