27 Commits

Author SHA1 Message Date
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
nolash
5dcf728701 Add singlecap contract + runner for all test combos 2021-06-05 11:58:35 +02:00
nolash
d6e71424f3 Apply single redistribution test 2021-06-05 08:59:21 +02:00
nolash
aeebdd348b Implement selectable deployment 2021-06-05 08:45:37 +02:00
nolash
093fcbccd5 Bump eth-erc20. add initial single and cap contract adaptations 2021-06-05 07:51:39 +02:00
nolash
3c871e5758 Upgrade chainlib 2021-06-05 06:48:19 +02:00
nolash
646a3ea007 Chainlib refactor tests complete (minus bench) 2021-06-04 15:59:42 +02:00
nolash
f299e25dcc Rehabilitate last test 2021-06-04 15:08:03 +02:00
nolash
4b780c81ad WIP refactor redistribution tests 2021-06-04 14:10:31 +02:00
nolash
68fa6488d2 WIP refactor redistribution tests 2021-06-04 14:03:07 +02:00
nolash
3471229ae6 Refactor actual period 2021-06-04 13:14:26 +02:00
nolash
71e94b5afb Refactor pure test on chainlib 2021-06-04 11:48:17 +02:00
nolash
4e4c0f1203 Complete refactor basic tests 2021-06-04 10:54:10 +02:00
nolash
32f536dec0 Refactor base amount test 2021-06-04 09:29:52 +02:00
nolash
d1a0a7e882 Refactor mint, minter tests 2021-06-04 09:24:05 +02:00
nolash
9f27f9e26a Rename module, add test base and two basic tests to chainlib 2021-06-04 09:05:08 +02:00
Louis Holbrook
b4dfb5a381 Merge branch 'lash/clean-and-doc' into 'master'
Lash/clean and doc

See merge request grassrootseconomics/sarafu-token!9
2021-05-06 08:35:58 +00:00
Louis Holbrook
1de39b90cf Lash/clean and doc 2021-05-06 08:35:58 +00:00
44 changed files with 3641 additions and 560 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
build/
dist/
*.egg-info
__pycache__
*.pyc
gmon.out
solidity/*.json
solidity/*.bin

View File

@@ -1 +1 @@
include sarafu_token/data/*
include erc20_demurrage_token/data/*

2
python/config/eth.ini Normal file
View File

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

View File

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

8
python/config/token.ini Normal file
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,4 @@
from .token import (
DemurrageToken,
DemurrageTokenSettings,
)

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,3 @@
import os
data_dir = os.path.realpath(os.path.dirname(__file__))

View File

@@ -0,0 +1,174 @@
"""Deploy sarafu token
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# standard imports
import sys
import os
import json
import argparse
import logging
# external imports
import confini
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
from chainlib.chain import ChainSpec
from chainlib.eth.nonce import (
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
)
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import receipt
from chainlib.eth.constant import ZERO_ADDRESS
# local imports
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')
default_config_dir = os.environ.get('CONFINI_DIR', '/usr/local/share/sarafu-token')
argparser = argparse.ArgumentParser()
argparser.add_argument('-c', '--config', dest='c', type=str, default=default_config_dir, help='configuration directory')
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', dest='sink_address', default=ZERO_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('--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', type=str, help='Token symbol')
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)
block_last = args.w
block_all = args.ww
# process config
config = confini.Config(args.c)
config.process()
args_override = {
'TOKEN_REDISTRIBUTION_PERIOD': getattr(args, 'redistribution_period'),
'TOKEN_DEMURRAGE_LEVEL': getattr(args, 'demurrage_level'),
'TOKEN_SUPPLY_LIMIT': getattr(args, 'supply_limit'),
'TOKEN_SYMBOL': getattr(args, 'symbol'),
'TOKEN_NAME': getattr(args, 'name'),
'TOKEN_DECIMALS': getattr(args, 'decimals'),
'TOKEN_SINK_ADDRESS': getattr(args, 'sink_address'),
'SESSION_CHAIN_SPEC': getattr(args, 'i'),
'ETH_PROVIDER': getattr(args, 'p'),
}
if config.get('TOKEN_NAME') == None:
logg.info('token name not set, using symbol {} as name'.format(config.get('TOKEN_SYMBOL')))
config.add(config.get('TOKEN_SYMBOL'), 'TOKEN_NAME', True)
config.dict_override(args_override, 'cli args')
if config.get('TOKEN_SUPPLY_LIMIT') == None:
config.add(0, 'TOKEN_SUPPLY_LIMIT', True)
if config.get('TOKEN_REDISTRIBUTION_PERIOD') == None:
config.add(10800, 'TOKEN_REDISTRIBUTION_PERIOD', True)
logg.debug('config loaded:\n{}'.format(config))
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=''
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)
chain_spec = ChainSpec.from_chain_str(args.i)
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)
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)
dummy = args.d
token_name = args.name
if token_name == None:
token_name = args.symbol
def main():
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,
settings,
redistribute=settings.period_minutes > 0,
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 r['status'] == 0:
sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you')
sys.exit(1)
# TODO: pass through translator for keys (evm tester uses underscore instead of camelcase)
address = r['contractAddress']
print(address)
else:
print(tx_hash_hex)
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,257 @@
# 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 crypto_dev_signer.keystore.dict import DictKeystore
from crypto_dev_signer.eth.signer import ReferenceSigner as 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.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.debug('added actor account #{}: {}'.format(i, address))
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)
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']
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.last_block += 1
self.last_timestamp += 1
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):
return self.period
def get_demurrage_modifier(self):
o = self.caller_contract.demurrage_amount(self.address, sender_address=self.caller_address)
r = self.rpc.do(o)
return float(self.caller_contract.parse_demurrage_amount(r) / (10 ** 38))
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_of(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.debug('warping to {}, {} from start'.format(target_timestamp, target_timestamp - self.start_timestamp))
self.last_timestamp = target_timestamp
self.eth_helper.time_travel(self.last_timestamp)
self.__next_block()
o = block_by_number(self.last_block)
r = self.rpc.do(o)
self.last_block = r['number']
block_base = self.last_block
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)
(tx_hash, o) = c.change_period(self.address, self.accounts[2])
self.rpc.do(o)
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

@@ -0,0 +1,378 @@
# standard imports
import os
import logging
# external imports
from chainlib.eth.tx import (
TxFactory,
TxFormat,
)
from chainlib.hash import keccak256_string_to_hex
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
abi_decode_single,
)
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.jsonrpc import jsonrpc_template
from eth_erc20 import ERC20
from hexathon import (
add_0x,
strip_0x,
)
# local imports
from erc20_demurrage_token.data import data_dir
logg = logging.getLogger(__name__)
class DemurrageTokenSettings:
def __init__(self):
self.name = None
self.symbol = None
self.decimals = None
self.demurrage_level = None
self.period_minutes = None
self.sink_address = None
class DemurrageToken(ERC20):
__abi = {}
__bytecode = {}
valid_modes = [
'MultiNocap',
'SingleNocap',
'MultiCap',
'SingleCap',
]
def constructor(self, sender_address, settings, redistribute=True, cap=0, tx_format=TxFormat.JSONRPC):
if int(cap) < 0:
raise ValueError('cap must be 0 or positive integer')
code = DemurrageToken.bytecode(multi=redistribute, cap=cap>0)
enc = ABIContractEncoder()
enc.string(settings.name)
enc.string(settings.symbol)
enc.uint256(settings.decimals)
enc.uint256(settings.demurrage_level)
enc.uint256(settings.period_minutes)
enc.address(settings.sink_address)
if cap > 0:
enc.uint256(cap)
code += enc.get()
tx = self.template(sender_address, None, use_nonce=True)
tx = self.set_code(tx, code)
return self.finalize(tx, tx_format)
@staticmethod
def gas(code=None):
return 3500000
@staticmethod
def __to_contract_name(multi, cap):
name = 'DemurrageToken'
if multi:
name += 'Multi'
else:
name += 'Single'
if cap:
name += 'Cap'
else:
name += 'Nocap'
logg.debug('bytecode name {}'.format(name))
return name
@staticmethod
def abi(multi=True, cap=False):
name = DemurrageToken.__to_contract_name(multi, cap)
if DemurrageToken.__abi.get(name) == None:
f = open(os.path.join(data_dir, name + '.json'), 'r')
DemurrageToken.__abi[name] = json.load(f)
f.close()
return DemurrageToken.__abi[name]
@staticmethod
def bytecode(multi=True, cap=False):
name = DemurrageToken.__to_contract_name(multi, cap)
if DemurrageToken.__bytecode.get(name) == None:
f = open(os.path.join(data_dir, name + '.bin'), 'r')
DemurrageToken.__bytecode[name] = f.read()
f.close()
return DemurrageToken.__bytecode[name]
def add_minter(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('addMinter')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
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 remove_minter(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('removeMinter')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
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 mint_to(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('mintTo')
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 to_base_amount(self, contract_address, value, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toBaseAmount')
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
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')
return o
def remainder(self, contract_address, parts, whole, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('remainder')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(parts)
enc.uint256(whole)
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')
return o
def redistributions(self, contract_address, idx, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('redistributions')
enc.typ(ABIContractType.UINT256)
enc.uint256(idx)
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')
return o
def account_period(self, contract_address, address, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('accountPeriod')
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')
return o
def to_redistribution_period(self, contract_address, redistribution, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionPeriod')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
return o
def to_redistribution_participants(self, contract_address, redistribution, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionParticipants')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
return o
def to_redistribution_supply(self, contract_address, redistribution, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionSupply')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
return o
def to_redistribution_demurrage_modifier(self, contract_address, redistribution, sender_address=ZERO_ADDRESS):
o = jsonrpc_template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionDemurrageModifier')
enc.typ(ABIContractType.BYTES32)
enc.bytes32(redistribution)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
return o
def base_balance_of(self, contract_address, address, sender_address=ZERO_ADDRESS):
o = jsonrpc_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')
return o
def apply_demurrage(self, contract_address, sender_address):
return self.transact_noarg('applyDemurrage', contract_address, sender_address)
def change_period(self, contract_address, sender_address):
return self.transact_noarg('changePeriod', contract_address, sender_address)
def apply_redistribution_on_account(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('applyRedistributionOnAccount')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
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 actual_period(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('actualPeriod', contract_address, sender_address=sender_address)
def period_start(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('periodStart', contract_address, sender_address=sender_address)
def period_duration(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('periodDuration', contract_address, sender_address=sender_address)
def demurrage_amount(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('demurrageAmount', 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)
@classmethod
def parse_actual_period(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_period_start(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_period_duration(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_demurrage_amount(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_remainder(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_to_base_amount(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_redistributions(self, v):
return abi_decode_single(ABIContractType.BYTES32, v)
@classmethod
def parse_account_period(self, v):
return abi_decode_single(ABIContractType.ADDRESS, v)
@classmethod
def parse_to_redistribution_period(self, v):
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)

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 = sim.from_units(10 ** 12) # 1 tn token units, with 6 decimal places
# 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)))
# 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

@@ -1,2 +1,3 @@
chainlib~=0.0.1a7
crypto-dev-signer~=0.4.13rc2
chainlib~=0.0.3rc3
eth-erc20~=0.0.9a4
crypto-dev-signer~=0.4.14b3

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,133 +0,0 @@
"""Deploys Sarafu token
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
import sys
import os
import json
import argparse
import logging
import time
from enum import Enum
# third-party imports
import web3
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore import DictKeystore
from crypto_dev_signer.eth.helper import EthTxExecutor
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
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('-e', action='store_true', help='Treat all transactions as essential')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='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('--name', dest='n', default='Giftable Token', type=str, help='Token name')
argparser.add_argument('--symbol', dest='s', default='GFT', type=str, help='Token symbol')
argparser.add_argument('--decimals', dest='d', default=18, type=int, help='Token decimals')
argparser.add_argument('--minter', action='append', type=str, help='Minter to add')
argparser.add_argument('--sink-address', type=str, help='Sink address (if not set, signer address is used)')
argparser.add_argument('--abi-dir', dest='abi_dir', type=str, default=data_dir, help='Directory containing bytecode and abi (default: {})'.format(data_dir))
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('taxlevel_minute', type=int, help='Tax level per minute in ppm')
argparser.add_argument('period_minutes', type=int, help='Redistribution period, in minutes')
args = argparser.parse_args()
if args.v:
logg.setLevel(logging.DEBUG)
block_last = args.w
block_all = args.ww
w3 = web3.Web3(web3.Web3.HTTPProvider(args.p))
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)
logg.debug('now have key for signer address {}'.format(signer_address))
signer = EIP155Signer(keystore)
chain_pair = args.i.split(':')
chain_id = int(chain_pair[1])
helper = EthTxExecutor(
w3,
signer_address,
signer,
chain_id,
block=args.ww,
)
#g = ERC20TxFactory(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id)
def main():
f = open(os.path.join(args.abi_dir, 'RedistributedDemurrageToken.json'), 'r')
abi = json.load(f)
f.close()
f = open(os.path.join(args.abi_dir, 'RedistributedDemurrageToken.bin'), 'r')
bytecode = f.read()
f.close()
sink_address = args.sink_address
if sink_address == None:
sink_address = signer_address
c = w3.eth.contract(abi=abi, bytecode=bytecode)
(tx_hash, rcpt) = helper.sign_and_send(
[
c.constructor(args.n, args.s, args.d, args.taxlevel_minute, args.period_minutes, sink_address).buildTransaction
],
force_wait=True,
)
logg.debug('tx hash {} rcpt {}'.format(tx_hash, rcpt))
address = rcpt.contractAddress
logg.debug('token contract mined {} {} {} {}'.format(address, args.n, args.s, args.d, args.taxlevel_minute, args.period_minutes, sink_address))
c = w3.eth.contract(abi=abi, address=address)
balance = c.functions.balanceOf(signer_address).call()
logg.info('balance {}: {} {}'.format(signer_address, balance, tx_hash))
if args.minter != None:
for a in args.minter:
if a == signer_address:
continue
(tx_hash, rcpt) = helper.sign_and_send(
[
c.functions.addMinter(a).buildTransaction,
],
)
logg.debug('minter add {} {}'.format(a, tx_hash))
if block_last:
helper.wait_for()
print(address)
sys.exit(0)
if __name__ == '__main__':
main()

View File

@@ -1,12 +1,15 @@
[metadata]
name = sarafu-token
version = 0.0.1a2
name = erc20-demurrage-token
version = 0.0.1b1
description = ERC20 token with redistributed continual demurrage
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/grassrootseconomics/sarafu-token
keywords =
ethereum
blockchain
cryptocurrency
erc20
classifiers =
Programming Language :: Python :: 3
Operating System :: OS Independent
@@ -24,18 +27,15 @@ licence_files =
include_package_data = True
python_requires = >= 3.6
packages =
sarafu_token
sarafu_token.runnable.legacy
install_requires =
chainlib~=0.0.1a7
crypto-dev-signer~=0.4.13rc2
web3==5.12.2
erc20_demurrage_token
erc20_demurrage_token.runnable
erc20_demurrage_token.data
[options.package_data]
* =
data/RedistributedDemurrageToken.bin
data/RedistributedDemurrageToken.json
data/DemurrageToken*.bin
data/DemurrageToken*.json
[options.entry_points]
console_scripts =
sarafu-token-deploy = sarafu_faucet.runnable.legacy.deploy:main
erc20-demurrage-token-deploy = erc20_demurrage_token.runnable.deploy:main

View File

@@ -1,5 +1,24 @@
from setuptools import setup
requirements = []
f = open('requirements.txt', 'r')
while True:
l = f.readline()
if l == '':
break
requirements.append(l.rstrip())
f.close()
test_requirements = []
f = open('test_requirements.txt', 'r')
while True:
l = f.readline()
if l == '':
break
test_requirements.append(l.rstrip())
f.close()
setup(
package_data={
'': [
@@ -7,4 +26,6 @@ setup(
],
},
include_package_data=True,
install_requires=requirements,
tests_require=test_requirements,
)

29
python/test.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/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_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_pure.py
ERC20_DEMURRAGE_TOKEN_TEST_MODE=$m python tests/test_redistribution.py
done
set +e

View File

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

213
python/tests/base.py Normal file
View File

@@ -0,0 +1,213 @@
# standard imports
import logging
import os
# external imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.tx import (
receipt,
)
from chainlib.eth.block import (
block_latest,
block_by_number,
)
from chainlib.eth.nonce import RPCNonceOracle
# local imports
from erc20_demurrage_token import (
DemurrageTokenSettings,
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
class TestDemurrage(EthTesterCase):
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.decimals = 6
self.settings.demurrage_level = TAX_LEVEL * (10 ** 32)
self.settings.period_minutes = PERIOD
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)
def deploy(self, interface, mode):
tx_hash = None
o = None
if mode == 'MultiNocap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=True, cap=0)
elif mode == 'SingleNocap':
(tx_hash, o) = interface.constructor(self.accounts[0], 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)
elif mode == 'SingleCap':
(tx_hash, o) = interface.constructor(self.accounts[0], self.settings, redistribute=False, cap=self.default_supply_cap)
else:
raise ValueError('Invalid mode "{}", valid are {}'.format(self.mode, DemurrageToken.valid_modes))
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(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)
self.start_time = r['timestamp']
logg.debug('contract address {} start block {} start time {}'.format(self.address, self.start_block, self.start_time))
def tearDown(self):
pass
class TestDemurrageDefault(TestDemurrage):
def setUp(self):
super(TestDemurrageDefault, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.mode = os.environ.get('ERC20_DEMURRAGE_TOKEN_TEST_MODE')
if self.mode == None:
self.mode = 'MultiNocap'
logg.debug('executing test setup default mode {}'.format(self.mode))
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
class TestDemurrageSingle(TestDemurrage):
def setUp(self):
super(TestDemurrageSingle, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.mode = os.environ.get('ERC20_DEMURRAGE_TOKEN_TEST_MODE')
single_valid_modes = [
'SingleNocap',
'SingleCap',
]
if self.mode != None:
if self.mode not in single_valid_modes:
raise ValueError('Invalid mode "{}" for "single" contract tests, valid are {}'.format(self.mode, single_valid_modes))
else:
self.mode = 'SingleNocap'
logg.debug('executing test setup demurragesingle mode {}'.format(self.mode))
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
class TestDemurrageCap(TestDemurrage):
def setUp(self):
super(TestDemurrageCap, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.mode = os.environ.get('ERC20_DEMURRAGE_TOKEN_TEST_MODE')
cap_valid_modes = [
'MultiCap',
'SingleCap',
]
if self.mode != None:
if self.mode not in cap_valid_modes:
raise ValueError('Invalid mode "{}" for "cap" contract tests, valid are {}'.format(self.mode, cap_valid_modes))
else:
self.mode = 'MultiCap'
logg.debug('executing test setup demurragecap mode {}'.format(self.mode))
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))
class TestDemurrageReal(TestDemurrage):
def setUp(self):
super(TestDemurrage, self).setUp()
self.tax_level = int(0.000050105908373373*(10**40))
self.period = 10800
self.period_seconds = self.period * 60
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
self.settings.period_minutes = 10800
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')
if self.mode == None:
self.mode = 'MultiNocap'
logg.debug('executing test setup default mode {}'.format(self.mode))
self.deploy(c, self.mode)
logg.info('deployed with mode {}'.format(self.mode))

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

@@ -5,206 +5,236 @@ import json
import logging
import datetime
# third-party imports
import web3
import eth_tester
import eth_abi
# 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 tests.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('eth.vm').setLevel(logging.WARNING)
testdir = os.path.dirname(__file__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = int(10000 * 2) # 2%
# calc "1-(0.98)^(1/518400)" <- 518400 = 30 days of blocks
# 0.00000003897127107225
#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month
PERIOD = 1
class Test(unittest.TestCase):
contract = None
def setUp(self):
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
'gas_limit': 9000000,
})
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
self.bytecode = f.read()
f.close()
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
self.abi = json.load(f)
f.close()
backend = eth_tester.PyEVMBackend(eth_params)
self.eth_tester = eth_tester.EthereumTester(backend)
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
self.w3 = web3.Web3(provider)
self.sink_address = self.w3.eth.accounts[9]
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
self.start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(self.start_block)
self.start_time = b['timestamp']
def tearDown(self):
pass
class TestBasic(TestDemurrageDefault):
def test_hello(self):
self.assertEqual(self.contract.functions.actualPeriod().call(), 1)
self.eth_tester.time_travel(self.start_time + 61)
self.assertEqual(self.contract.functions.actualPeriod().call(), 2)
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)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
def test_apply_demurrage(self):
modifier = 10 * (10 ** 37)
#demurrage_modifier = self.contract.functions.demurrageModifier().call()
#demurrage_modifier &= (1 << 128) - 1
demurrage_amount = self.contract.functions.demurrageAmount().call()
#self.assertEqual(modifier, demurrage_modifier)
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.eth_tester.time_travel(self.start_time + 59)
#demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_amount = self.contract.functions.demurrageAmount().call()
#demurrage_modifier &= (1 << 128) - 1
#self.assertEqual(modifier, demurrage_modifier)
self.backend.time_travel(self.start_time + self.period_seconds - 1)
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.eth_tester.time_travel(self.start_time + 61)
tx_hash = self.contract.functions.applyDemurrage().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
#demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_amount = self.contract.functions.demurrageAmount().call()
#demurrage_modifier &= (1 << 128) - 1
#self.assertEqual(int(98 * (10 ** 36)), demurrage_modifier)
self.assertEqual(int(98 * (10 ** 36)), demurrage_amount)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
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)
def test_mint(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
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)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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)
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 976).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 976)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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, 2000)
self.eth_tester.time_travel(self.start_time + 61)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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 = 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, int(2000 * 0.98))
def test_minter_control(self):
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[2], 1024).transact({'from': self.w3.eth.accounts[1]})
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
tx_hash = self.contract.functions.addMinter(self.w3.eth.accounts[1]).transact({'from': self.w3.eth.accounts[1]})
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
tx_hash = self.contract.functions.addMinter(self.w3.eth.accounts[1]).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
(tx_hash, o) = c.add_minter(self.address, self.accounts[1], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
tx_hash = self.contract.functions.addMinter(self.w3.eth.accounts[2]).transact({'from': self.w3.eth.accounts[1]})
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[2], 1024).transact({'from': self.w3.eth.accounts[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.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
tx_hash = self.contract.functions.addMinter(self.w3.eth.accounts[1]).transact({'from': self.w3.eth.accounts[2]})
(tx_hash, o) = c.add_minter(self.address, self.accounts[1], self.accounts[2])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
tx_hash = self.contract.functions.removeMinter(self.w3.eth.accounts[1]).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[2], 1024).transact({'from': self.w3.eth.accounts[1]})
(tx_hash, o) = c.remove_minter(self.address, self.accounts[1], 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.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
def test_base_amount(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.eth_tester.time_travel(self.start_time + 61)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
self.rpc.do(o)
self.contract.functions.applyDemurrage().transact()
#demurrage_modifier = self.contract.functions.demurrageModifier().call()
#demurrage_amount = self.contract.functions.toDemurrageAmount(demurrage_modifier).call()
demurrage_amount = self.contract.functions.demurrageAmount().call()
a = self.contract.functions.toBaseAmount(1000).call();
self.assertEqual(a, 1020)
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 = c.to_base_amount(self.address, 1000, sender_address=self.accounts[0])
r = self.rpc.do(o)
amount = c.parse_to_base_amount(r)
self.assertEqual(amount, 1020)
def test_transfer(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
self.rpc.do(o)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 524)
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)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
balance_bob = self.contract.functions.balanceOf(self.w3.eth.accounts[2]).call()
self.assertEqual(balance_bob, 500)
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, 524)
tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
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)
self.assertEqual(balance, 500)
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], self.accounts[1], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
def test_transfer_from(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
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)
self.rpc.do(o)
tx_hash = self.contract.functions.approve(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.approve(self.address, self.accounts[1], self.accounts[2], 500)
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)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 1024)
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_from(self.address, self.accounts[2], self.accounts[1], self.accounts[3], 500)
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, 524)
tx_hash = self.contract.functions.transferFrom(self.w3.eth.accounts[1], self.w3.eth.accounts[3], 500).transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 524)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[3]).call()
self.assertEqual(balance_alice, 500)
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, 500)
if __name__ == '__main__':

67
python/tests/test_cap.py Normal file
View File

@@ -0,0 +1,67 @@
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
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 tests.base import TestDemurrageCap
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestCap(TestDemurrageCap):
def test_cap_set(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.supply_cap(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
cap = c.parse_supply_cap(r)
self.assertEqual(cap, self.default_supply_cap)
def test_cap(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], self.default_supply_cap)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
def test_cap_first(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], self.default_supply_cap + 1)
r = 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

@@ -4,76 +4,64 @@ import unittest
import json
import logging
# third-party imports
import web3
import eth_tester
import eth_abi
# 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 tests.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('eth.vm').setLevel(logging.WARNING)
testdir = os.path.dirname(__file__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = 10000 * 2 # 2%
#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month
PERIOD = 1
class Test(unittest.TestCase):
contract = None
def setUp(self):
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
'gas_limit': 9000000,
})
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
self.bytecode = f.read()
f.close()
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
self.abi = json.load(f)
f.close()
backend = eth_tester.PyEVMBackend(eth_params)
self.eth_tester = eth_tester.EthereumTester(backend)
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
self.w3 = web3.Web3(provider)
self.sink_address = self.w3.eth.accounts[9]
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
self.start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(self.start_block)
self.start_time = b['timestamp']
def tearDown(self):
pass
class TestPeriod(TestDemurrageDefault):
def test_period(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
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)
self.eth_tester.time_travel(self.start_time + 61)
tx_hash = self.contract.functions.changePeriod().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
self.backend.time_travel(self.start_time + 61)
redistribution = self.contract.functions.redistributions(1).call()
self.assertEqual(2, self.contract.functions.toRedistributionPeriod(redistribution).call())
self.assertEqual(2, self.contract.functions.actualPeriod().call())
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, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution = c.parse_redistributions(r)
o = c.to_redistribution_period(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_period(r)
self.assertEqual(2, period)
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution = c.parse_redistributions(r)
o = c.to_redistribution_period(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_period(r)
self.assertEqual(2, period)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_actual_period(r)
self.assertEqual(2, period)
if __name__ == '__main__':

View File

@@ -5,104 +5,66 @@ import json
import logging
import math
# third-party imports
import web3
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.error import JSONRPCException
import eth_tester
import eth_abi
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('eth.vm').setLevel(logging.WARNING)
testdir = os.path.dirname(__file__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = int((10000 * 2) * (10 ** 32)) # 2%
PERIOD = 10
class Test(unittest.TestCase):
contract = None
def setUp(self):
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
'gas_limit': 9000000,
})
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
self.bytecode = f.read()
f.close()
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
self.abi = json.load(f)
f.close()
backend = eth_tester.PyEVMBackend(eth_params)
self.eth_tester = eth_tester.EthereumTester(backend)
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
self.w3 = web3.Web3(provider)
self.sink_address = self.w3.eth.accounts[9]
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL, PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
self.start_block = self.w3.eth.blockNumber
def tearDown(self):
pass
@unittest.skip('this function has been removed from contract')
def test_tax_period(self):
t = self.contract.functions.taxLevel().call()
logg.debug('taxlevel {}'.format(t))
a = self.contract.functions.toDemurrageAmount(1000000, 0).call()
self.assertEqual(a, 1000000)
a = self.contract.functions.toDemurrageAmount(1000000, 1).call()
self.assertEqual(a, 980000)
a = self.contract.functions.toDemurrageAmount(1000000, 2).call()
self.assertEqual(a, 960400)
a = self.contract.functions.toDemurrageAmount(980000, 1).call()
self.assertEqual(a, 960400)
class Test(TestDemurrageDefault):
def test_fractional_state(self):
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
self.contract.functions.remainder(2, 1).call();
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
with self.assertRaises(eth_tester.exceptions.TransactionFailed):
remainder = self.contract.functions.remainder(0, 100001).call();
with self.assertRaises(JSONRPCException):
o = c.remainder(self.address, 2, 1, sender_address=self.accounts[0])
self.rpc.do(o)
with self.assertRaises(JSONRPCException):
o = c.remainder(self.address, 0, 100001, sender_address=self.accounts[0])
self.rpc.do(o)
remainder = self.contract.functions.remainder(1, 2).call();
o = c.remainder(self.address, 1, 2, sender_address=self.accounts[0])
r = self.rpc.do(o)
remainder = c.parse_remainder(r)
self.assertEqual(remainder, 0);
whole = 5000001
parts = 20000
expect = whole - (math.floor(whole/parts) * parts)
remainder = self.contract.functions.remainder(parts, whole).call();
o = c.remainder(self.address, parts, whole, sender_address=self.accounts[0])
r = self.rpc.do(o)
remainder = c.parse_remainder(r)
self.assertEqual(remainder, expect)
parts = 30000
expect = whole - (math.floor(whole/parts) * parts)
remainder = self.contract.functions.remainder(parts, whole).call();
o = c.remainder(self.address, parts, whole, sender_address=self.accounts[0])
r = self.rpc.do(o)
remainder = c.parse_remainder(r)
self.assertEqual(remainder, expect)
parts = 40001
expect = whole - (math.floor(whole/parts) * parts)
remainder = self.contract.functions.remainder(parts, whole).call();
o = c.remainder(self.address, parts, whole, sender_address=self.accounts[0])
r = self.rpc.do(o)
remainder = c.parse_remainder(r)
self.assertEqual(remainder, expect)
if __name__ == '__main__':
unittest.main()

View File

@@ -4,192 +4,257 @@ import unittest
import json
import logging
# third-party imports
import web3
import eth_tester
import eth_abi
# 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
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 tests.base import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('eth.vm').setLevel(logging.WARNING)
testdir = os.path.dirname(__file__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = 10000 * 2 # 2%
#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month
PERIOD = 1
class TestRedistribution(TestDemurrageDefault):
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)
class Test(unittest.TestCase):
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pactual = c.parse_actual_period(r)
contract = None
o = c.period_start(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pstart = c.parse_actual_period(r)
def setUp(self):
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
'gas_limit': 9000000,
})
o = c.period_duration(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
pduration = c.parse_actual_period(r)
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
self.bytecode = f.read()
f.close()
o = block_latest()
blocknumber = self.rpc.do(o)
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
self.abi = json.load(f)
f.close()
backend = eth_tester.PyEVMBackend(eth_params)
self.eth_tester = eth_tester.EthereumTester(backend)
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
self.w3 = web3.Web3(provider)
self.sink_address = self.w3.eth.accounts[9]
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
self.start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(self.start_block)
self.start_time = b['timestamp']
def tearDown(self):
pass
def debug_periods(self):
pactual = self.contract.functions.actualPeriod().call()
pstart = self.contract.functions.periodStart().call()
pduration = self.contract.functions.periodDuration().call()
blocknumber = self.w3.eth.blockNumber;
logg.debug('actual {} start {} duration {} blocknumber {}'.format(pactual, pstart, pduration, blocknumber))
# TODO: check receipt log outputs
def test_redistribution_storage(self):
redistribution = self.contract.functions.redistributions(0).call();
self.assertEqual(redistribution.hex(), '000000000000000000000000f424000000000000000000000000000000000001')
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')
self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact()
self.contract.functions.mintTo(self.w3.eth.accounts[2], 1000000).transact()
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000000)
r = self.rpc.do(o)
external_address = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
tx_hash = self.contract.functions.transfer(external_address, 1000000).transact({'from': self.w3.eth.accounts[2]})
tx_hash = self.contract.functions.transfer(external_address, 999999).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
logg.debug('tx before {}'.format(r))
self.assertEqual(r.status, 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1000000)
r = self.rpc.do(o)
self.eth_tester.time_travel(self.start_time + 61)
external_address = to_checksum_address('0x' + os.urandom(20).hex())
redistribution = self.contract.functions.redistributions(0).call();
self.assertEqual(redistribution.hex(), '000000000000000000000000f42400000000010000000000001e848000000001')
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)
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[0], 1000000).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
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')
redistribution = self.contract.functions.redistributions(1).call()
self.assertEqual(redistribution.hex(), '000000000000000000000000ef4200000000000000000000002dc6c000000002')
def test_redistribution_balance_on_zero_participants(self):
supply = 1000000000000
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], supply).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
supply = self.default_supply
self.eth_tester.time_travel(self.start_time + 61)
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)
tx_hash = self.contract.functions.applyDemurrage().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
logg.debug('r {}'.format(r))
self.assertEqual(r.status, 1)
tx_hash = self.contract.functions.changePeriod().transact()
rr = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(rr.status, 1)
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)
redistribution = self.contract.functions.redistributions(0).call();
supply = self.contract.functions.totalSupply().call()
(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)
sink_increment = int(supply * (TAX_LEVEL / 1000000))
for l in r['logs']:
if l.topics[0].hex() == '0xa0717e54e02bd9829db5e6e998aec0ae9de796b8d150a3cc46a92ab869697755': # event Decayed(uint256,uint256,uint256,uint256)
period = int.from_bytes(l.topics[1], 'big')
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(l.data[2:])
b = bytes.fromhex(strip_0x(l['data']))
remainder = int.from_bytes(b, 'big')
self.assertEqual(remainder, int((1000000 - TAX_LEVEL) * (10 ** 32)))
logg.debug('period {} remainder {}'.format(period, remainder))
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)
sink_balance = self.contract.functions.balanceOf(self.sink_address).call()
logg.debug('{} {}'.format(sink_increment, sink_balance))
self.assertEqual(sink_balance, int(sink_increment * 0.98))
self.assertEqual(sink_balance, int(sink_increment * (1000000 - TAX_LEVEL) / 1000000))
self.assertEqual(sink_balance, int(sink_increment * (1000000 - self.tax_level) / 1000000))
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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):
self.contract.functions.mintTo(self.w3.eth.accounts[i], mint_amount).transact()
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i], mint_amount)
self.rpc.do(o)
z += mint_amount
initial_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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 = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
self.contract.functions.transfer(external_address, spend_amount).transact({'from': self.w3.eth.accounts[1]})
tx_hash = self.contract.functions.transfer(external_address, spend_amount).transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
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!
self.contract.functions.transfer(self.w3.eth.accounts[3], spend_amount).transact({'from': self.w3.eth.accounts[3]})
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!
self.contract.functions.transfer(external_address, spend_amount-1).transact({'from': self.w3.eth.accounts[4]})
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.assertEqual(r.status, 1)
self.eth_tester.time_travel(self.start_time + 61)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
self.contract.functions.applyDemurrage().transact()
self.contract.functions.changePeriod().transact()
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[4])
self.rpc.do(o)
bummer_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[3]).call()
self.assertEqual(bummer_balance, mint_amount - (mint_amount * (TAX_LEVEL / 1000000)))
(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))
bummer_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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 * (TAX_LEVEL / 1000000)))
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 = self.contract.functions.applyRedistributionOnAccount(self.w3.eth.accounts[1]).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
logg.debug('log {}'.format(r.logs))
(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)
self.contract.functions.applyRedistributionOnAccount(self.w3.eth.accounts[2]).transact()
# 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)
redistribution_data = self.contract.functions.redistributions(0).call()
logg.debug('redist data {}'.format(redistribution_data.hex()))
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))
account_period_data = self.contract.functions.accountPeriod(self.w3.eth.accounts[1]).call()
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))
actual_period = self.contract.functions.actualPeriod().call()
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) * (TAX_LEVEL / 1000000))
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 * (TAX_LEVEL / 1000000)))
spender_new_decayed_balance = int(spender_new_base_balance - (spender_new_base_balance * (self.tax_level / 1000000)))
spender_actual_balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
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)
@@ -197,5 +262,3 @@ class Test(unittest.TestCase):
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,84 @@
# standard imports
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from tests.base import TestDemurrageReal
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistribution(TestDemurrageReal):
def test_simple_example(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], 100000000)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 100000000)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
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)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
(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)
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
logg.debug('redistribution {}'.format(redistribution))
o = c.to_redistribution_period(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_item(r)
logg.debug('period {}'.format(period))
o = c.to_redistribution_participants(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
participants = c.parse_to_redistribution_item(r)
logg.debug('participants {}'.format(participants))
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)
logg.debug('supply {}'.format(supply))
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
modifier = c.parse_to_redistribution_item(r)
logg.debug('modifier {}'.format(modifier))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,95 @@
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
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 tests.base import TestDemurrageSingle
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistributionSingle(TestDemurrageSingle):
def test_single_even_if_multiple(self):
mint_amount = 100000000
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
for i in range(3):
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i+1], mint_amount)
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, int(mint_amount) * 0.1)
r = self.rpc.do(o)
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], external_address, int(mint_amount) * 0.2)
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[3])
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[3])
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, mint_amount - (mint_amount * (self.tax_level / 1000000)))
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)
base_amount = mint_amount - int(mint_amount * 0.1)
self.assertEqual(balance, (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)
base_amount = mint_amount - int(mint_amount * 0.2)
self.assertEqual(balance, (base_amount - (base_amount * (self.tax_level / 1000000))))
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
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)))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,618 @@
pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenMultiCap {
// 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
// Cached demurrage amount, ppm with 38 digit resolution
uint128 public demurrageAmount;
// Cached demurrage period; the period for which demurrageAmount was calculated
uint128 public demurragePeriod;
// Implements EIP172
address public owner;
address newOwner;
// Implements ERC20
string public name;
// Implements ERC20
string public symbol;
// Implements ERC20
uint256 public decimals;
// Implements ERC20
uint256 public totalSupply;
// Maximum amount of tokens that can be minted
uint256 public supplyCap;
// Minimum amount of (demurraged) tokens an account must spend to participate in redistribution for a particular period
uint256 public minimumParticipantSpend;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
// Duration of a single redistribution period in seconds
uint256 public immutable periodDuration;
// Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
// Storage for ERC20 approve/transferFrom methods
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
// Address to send unallocated redistribution tokens
address sinkAddress;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// Implements ERC20
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// New tokens minted
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
// New demurrage cache milestone calculated
event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount);
// When a new period threshold has been crossed
event Period(uint256 _period);
// Redistribution applied on a single eligible account
event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value);
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
// 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 {
// ACL setup
owner = msg.sender;
minter[owner] = true;
// ERC20 setup
name = _name;
symbol = _symbol;
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
supplyCap = _supplyCap;
sinkAddress = _defaultSinkAddress;
minimumParticipantSpend = 10 ** uint256(_decimals);
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(msg.sender == owner);
minter[_minter] = true;
return true;
}
// Given address will no longer be allowed to call the mintTo() function
function removeMinter(address _minter) public returns (bool) {
require(msg.sender == owner || _minter == msg.sender);
minter[_minter] = false;
return true;
}
/// Implements ERC20
function balanceOf(address _account) public view returns (uint256) {
uint256 baseBalance;
uint256 currentDemurragedAmount;
uint256 periodCount;
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return uint256(account[_account]) & maskAccountValue;
}
/// Increases base balance for a single account
function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
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);
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
newBalance = oldBalance - _delta;
workAccount &= (~maskAccountValue);
workAccount |= (newBalance & maskAccountValue);
account[_account] = bytes32(workAccount);
return true;
}
// Creates new tokens out of thin air, and allocates them to the given address
// Triggers tax
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(minter[msg.sender]);
require(_amount + totalSupply <= supplyCap);
changePeriod();
baseAmount = _amount;
totalSupply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
saveRedistributionSupply();
return true;
}
// 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;
redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & maskRedistributionPeriod;
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
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 demurrage modifier 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;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (totalSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true;
}
// Get the demurrage period of the current block number
function actualPeriod() public view returns (uint128) {
return uint128((block.timestamp - periodStart) / periodDuration + 1);
}
// Add an entered demurrage period to the redistribution array
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[redistributions.length-1];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return bytes32(0x00);
}
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;
}
// 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;
}
// 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;
}
// Called in the edge case where participant number is 0. It will override the participant count to 1.
// Returns the remainder sent to the sink address
function applyDefaultRedistribution(bytes32 _redistribution) private returns (uint256) {
uint256 redistributionSupply;
uint256 redistributionPeriod;
uint256 unit;
uint256 truncatedResult;
redistributionSupply = toRedistributionSupply(_redistribution);
unit = (redistributionSupply * taxLevel) / 1000000;
truncatedResult = (unit * 1000000) / taxLevel;
if (truncatedResult < redistributionSupply) {
redistributionPeriod = toRedistributionPeriod(_redistribution); // since we reuse period here, can possibly be optimized by passing period instead
redistributions[redistributionPeriod-1] &= 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);
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 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;
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
// Return timestamp of start of period threshold
function getPeriodTimeDelta(uint256 _periodCount) public view returns (uint256) {
return periodStart + (_periodCount * periodDuration);
}
// Amount of demurrage cycles inbetween the current timestamp and the given target time
function demurrageCycles(uint256 _target) public view returns (uint256) {
return (block.timestamp - _target) / 60;
}
// Recalculate the demurrage modifier for the new period
// After this, all REPORTED balances will have been reduced by the corresponding ratio (but the effecive totalsupply stays the same)
function changePeriod() public returns (bool) {
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;
}
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);
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);
}
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;
}
// 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;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000);
}
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;
}
// 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;
}
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
changePeriod();
applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, _value);
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);
emit Transfer(msg.sender, _to, _value);
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);
result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result;
}
// ERC20 transfer backend for transfer, transferFrom
function transferBase(address _from, address _to, uint256 _value) private returns (bool) {
uint256 period;
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
period = actualPeriod();
if (_value >= minimumParticipantSpend && accountPeriod(_from) != period && _from != _to) {
registerAccountPeriod(_from, period);
}
return true;
}
// Implements EIP173
function transferOwnership(address _newOwner) public returns (bool) {
require(msg.sender == owner);
newOwner = _newOwner;
}
// Implements OwnedAccepter
function acceptOwnership() public returns (bool) {
address oldOwner;
require(msg.sender == newOwner);
oldOwner = owner;
owner = newOwner;
newOwner = address(0);
emit OwnershipTransferred(oldOwner, owner);
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20
return true;
}
if (_sum == 0x449a52f8) { // Minter
return true;
}
if (_sum == 0x01ffc9a7) { // EIP165
return true;
}
if (_sum == 0x9493f8b2) { // EIP173
return true;
}
if (_sum == 0x37a47be4) { // OwnedAccepter
return true;
}
return false;
}
}

View File

@@ -2,7 +2,7 @@ pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
contract RedistributedDemurrageToken {
contract DemurrageTokenMultiNocap {
// Redistribution bit field, with associated shifts and masks
// (Uses sub-byte boundaries)
@@ -35,6 +35,8 @@ contract RedistributedDemurrageToken {
// Implements EIP172
address public owner;
address newOwner;
// Implements ERC20
string public name;
@@ -93,6 +95,9 @@ contract RedistributedDemurrageToken {
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
// 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 {
// ACL setup
owner = msg.sender;
@@ -509,7 +514,7 @@ contract RedistributedDemurrageToken {
return (_value * ppmDivider * 1000000) / demurrageAmount;
}
// ERC20, triggers tax and/or redistribution
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
@@ -522,7 +527,7 @@ contract RedistributedDemurrageToken {
return true;
}
// ERC20, triggers tax and/or redistribution
// Implements ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
@@ -537,7 +542,7 @@ contract RedistributedDemurrageToken {
}
// ERC20, triggers tax and/or redistribution
// Implements ERC20, triggers tax and/or redistribution
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
@@ -566,4 +571,41 @@ contract RedistributedDemurrageToken {
}
return true;
}
// Implements EIP173
function transferOwnership(address _newOwner) public returns (bool) {
require(msg.sender == owner);
newOwner = _newOwner;
}
// Implements OwnedAccepter
function acceptOwnership() public returns (bool) {
address oldOwner;
require(msg.sender == newOwner);
oldOwner = owner;
owner = newOwner;
newOwner = address(0);
emit OwnershipTransferred(oldOwner, owner);
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20
return true;
}
if (_sum == 0x449a52f8) { // Minter
return true;
}
if (_sum == 0x01ffc9a7) { // EIP165
return true;
}
if (_sum == 0x9493f8b2) { // EIP173
return true;
}
if (_sum == 0x37a47be4) { // OwnedAccepter
return true;
}
return false;
}
}

View File

@@ -0,0 +1,475 @@
pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleCap {
// Redistribution bit field, with associated shifts and masks
// (Uses sub-byte boundaries)
bytes32[] public redistributions; // 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 shiftRedistributionDemurrage = 140;
uint256 constant maskRedistributionDemurrage = 0x000000000000000000000000fffff00000000000000000000000000000000000; // ((1 << 20) - 1) << 140
// 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;
// Implements EIP172
address public owner;
address newOwner;
// Implements ERC20
string public name;
// Implements ERC20
string public symbol;
// Implements ERC20
uint256 public decimals;
// Implements ERC20
uint256 public totalSupply;
// Maximum amount of tokens that can be minted
uint256 public supplyCap;
// Minimum amount of (demurraged) tokens an account must spend to participate in redistribution for a particular period
uint256 public minimumParticipantSpend;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
// Duration of a single redistribution period in seconds
uint256 public immutable periodDuration;
// Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
// Storage for ERC20 approve/transferFrom methods
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
// Address to send unallocated redistribution tokens
address sinkAddress;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// Implements ERC20
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// New tokens minted
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
// New demurrage cache milestone calculated
event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount);
// When a new period threshold has been crossed
event Period(uint256 _period);
// Redistribution applied on a single eligible account
event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value);
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
// 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 {
// ACL setup
owner = msg.sender;
minter[owner] = true;
// ERC20 setup
name = _name;
symbol = _symbol;
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
supplyCap = _supplyCap;
sinkAddress = _defaultSinkAddress;
minimumParticipantSpend = 10 ** uint256(_decimals);
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(msg.sender == owner);
minter[_minter] = true;
return true;
}
// Given address will no longer be allowed to call the mintTo() function
function removeMinter(address _minter) public returns (bool) {
require(msg.sender == owner || _minter == msg.sender);
minter[_minter] = false;
return true;
}
/// Implements ERC20
function balanceOf(address _account) public view returns (uint256) {
uint256 baseBalance;
uint256 currentDemurragedAmount;
uint256 periodCount;
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return account[_account];
}
/// Increases base balance for a single account
function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
account[_account] = oldBalance + _delta;
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
account[_account] = oldBalance - _delta;
return true;
}
// Creates new tokens out of thin air, and allocates them to the given address
// Triggers tax
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(minter[msg.sender], 'ERR_ACCESS');
require(_amount + totalSupply <= supplyCap, 'ERR_CAP');
changePeriod();
baseAmount = _amount;
totalSupply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
saveRedistributionSupply();
return true;
}
// Deserializes the redistribution word
// uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) {
bytes32 redistribution;
redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & maskRedistributionPeriod;
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(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;
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
uint256 currentRedistribution;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (totalSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true;
}
// Get the demurrage period of the current block number
function actualPeriod() public view returns (uint128) {
return uint128((block.timestamp - periodStart) / periodDuration + 1);
}
// Add an entered demurrage period to the redistribution array
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[redistributions.length-1];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return bytes32(0x00);
}
return lastRedistribution;
}
// 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
}
increaseBaseBalance(sinkAddress, unit / ppmDivider);
return unit;
}
// 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;
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
// Return timestamp of start of period threshold
function getPeriodTimeDelta(uint256 _periodCount) public view returns (uint256) {
return periodStart + (_periodCount * periodDuration);
}
// Amount of demurrage cycles inbetween the current timestamp and the given target time
function demurrageCycles(uint256 _target) public view returns (uint256) {
return (block.timestamp - _target) / 60;
}
// Recalculate the demurrage modifier for the new period
function changePeriod() public returns (bool) {
bytes32 currentRedistribution;
bytes32 nextRedistribution;
uint256 currentPeriod;
uint256 currentDemurrageAmount;
uint256 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 periodTimestamp;
uint256 nextPeriod;
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
return false;
}
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);
redistributions.push(nextRedistribution);
applyDefaultRedistribution(currentRedistribution);
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;
}
// 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;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000);
}
return (valueFactor * _value) / 1000000;
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
return (_value * ppmDivider * 1000000) / demurrageAmount;
}
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
changePeriod();
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, _value);
return true;
}
// Implements ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
changePeriod();
baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue);
emit Transfer(msg.sender, _to, _value);
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();
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result;
}
// ERC20 transfer backend for transfer, transferFrom
function transferBase(address _from, address _to, uint256 _value) private returns (bool) {
uint256 period;
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
period = actualPeriod();
return true;
}
// Implements EIP173
function transferOwnership(address _newOwner) public returns (bool) {
require(msg.sender == owner);
newOwner = _newOwner;
}
// Implements OwnedAccepter
function acceptOwnership() public returns (bool) {
address oldOwner;
require(msg.sender == newOwner);
oldOwner = owner;
owner = newOwner;
newOwner = address(0);
emit OwnershipTransferred(oldOwner, owner);
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20
return true;
}
if (_sum == 0x449a52f8) { // Minter
return true;
}
if (_sum == 0x01ffc9a7) { // EIP165
return true;
}
if (_sum == 0x9493f8b2) { // EIP173
return true;
}
if (_sum == 0x37a47be4) { // OwnedAccepter
return true;
}
return false;
}
}

View File

@@ -0,0 +1,471 @@
pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleNocap {
// Redistribution bit field, with associated shifts and masks
// (Uses sub-byte boundaries)
bytes32[] public redistributions; // 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 shiftRedistributionDemurrage = 140;
uint256 constant maskRedistributionDemurrage = 0x000000000000000000000000fffff00000000000000000000000000000000000; // ((1 << 20) - 1) << 140
// 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;
// Implements EIP172
address public owner;
address newOwner;
// Implements ERC20
string public name;
// Implements ERC20
string public symbol;
// Implements ERC20
uint256 public decimals;
// Implements ERC20
uint256 public totalSupply;
// Minimum amount of (demurraged) tokens an account must spend to participate in redistribution for a particular period
uint256 public minimumParticipantSpend;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
// Duration of a single redistribution period in seconds
uint256 public immutable periodDuration;
// Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
// Storage for ERC20 approve/transferFrom methods
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
// Address to send unallocated redistribution tokens
address sinkAddress;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// Implements ERC20
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// New tokens minted
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
// New demurrage cache milestone calculated
event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount);
// When a new period threshold has been crossed
event Period(uint256 _period);
// Redistribution applied on a single eligible account
event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value);
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
// 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 {
// ACL setup
owner = msg.sender;
minter[owner] = true;
// ERC20 setup
name = _name;
symbol = _symbol;
decimals = _decimals;
// Demurrage setup
periodStart = block.timestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = uint128(ppmDivider * 1000000); // Represents 38 decimal places
demurragePeriod = 1;
taxLevel = _taxLevelMinute; // Represents 38 decimal places
bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
sinkAddress = _defaultSinkAddress;
minimumParticipantSpend = 10 ** uint256(_decimals);
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(msg.sender == owner);
minter[_minter] = true;
return true;
}
// Given address will no longer be allowed to call the mintTo() function
function removeMinter(address _minter) public returns (bool) {
require(msg.sender == owner || _minter == msg.sender);
minter[_minter] = false;
return true;
}
/// Implements ERC20
function balanceOf(address _account) public view returns (uint256) {
uint256 baseBalance;
uint256 currentDemurragedAmount;
uint256 periodCount;
baseBalance = baseBalanceOf(_account);
periodCount = actualPeriod() - demurragePeriod;
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
}
/// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return account[_account];
}
/// Increases base balance for a single account
function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
account[_account] = oldBalance + _delta;
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
account[_account] = oldBalance - _delta;
return true;
}
// Creates new tokens out of thin air, and allocates them to the given address
// Triggers tax
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(minter[msg.sender]);
changePeriod();
baseAmount = _amount;
totalSupply += _amount;
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
saveRedistributionSupply();
return true;
}
// Deserializes the redistribution word
// uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) {
bytes32 redistribution;
redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & maskRedistributionPeriod;
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(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;
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
uint256 currentRedistribution;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= (totalSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true;
}
// Get the demurrage period of the current block number
function actualPeriod() public view returns (uint128) {
return uint128((block.timestamp - periodStart) / periodDuration + 1);
}
// Add an entered demurrage period to the redistribution array
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[redistributions.length-1];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return bytes32(0x00);
}
return lastRedistribution;
}
// 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
}
increaseBaseBalance(sinkAddress, unit / ppmDivider);
return unit;
}
// 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;
epochPeriodCount = actualPeriod();
periodCount = epochPeriodCount - demurragePeriod;
if (periodCount == 0) {
return false;
}
lastDemurrageAmount = demurrageAmount;
demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurragePeriod = epochPeriodCount;
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
return true;
}
// Return timestamp of start of period threshold
function getPeriodTimeDelta(uint256 _periodCount) public view returns (uint256) {
return periodStart + (_periodCount * periodDuration);
}
// Amount of demurrage cycles inbetween the current timestamp and the given target time
function demurrageCycles(uint256 _target) public view returns (uint256) {
return (block.timestamp - _target) / 60;
}
// Recalculate the demurrage modifier for the new period
function changePeriod() public returns (bool) {
bytes32 currentRedistribution;
bytes32 nextRedistribution;
uint256 currentPeriod;
uint256 currentDemurrageAmount;
uint256 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 periodTimestamp;
uint256 nextPeriod;
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
return false;
}
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);
redistributions.push(nextRedistribution);
applyDefaultRedistribution(currentRedistribution);
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;
}
// 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;
for (uint256 i = 0; i < _period; i++) {
valueFactor = valueFactor - ((valueFactor * truncatedTaxLevel) / 1000000);
}
return (valueFactor * _value) / 1000000;
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
return (_value * ppmDivider * 1000000) / demurrageAmount;
}
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
changePeriod();
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, _value);
return true;
}
// Implements ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
changePeriod();
baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue);
emit Transfer(msg.sender, _to, _value);
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();
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result;
}
// ERC20 transfer backend for transfer, transferFrom
function transferBase(address _from, address _to, uint256 _value) private returns (bool) {
uint256 period;
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
period = actualPeriod();
return true;
}
// Implements EIP173
function transferOwnership(address _newOwner) public returns (bool) {
require(msg.sender == owner);
newOwner = _newOwner;
}
// Implements OwnedAccepter
function acceptOwnership() public returns (bool) {
address oldOwner;
require(msg.sender == newOwner);
oldOwner = owner;
owner = newOwner;
newOwner = address(0);
emit OwnershipTransferred(oldOwner, owner);
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20
return true;
}
if (_sum == 0x449a52f8) { // Minter
return true;
}
if (_sum == 0x01ffc9a7) { // EIP165
return true;
}
if (_sum == 0x9493f8b2) { // EIP173
return true;
}
if (_sum == 0x37a47be4) { // OwnedAccepter
return true;
}
return false;
}
}

View File

@@ -1,16 +1,38 @@
SOLC = /usr/bin/solc
all:
$(SOLC) RedistributedDemurrageToken.sol --abi --evm-version byzantium | awk 'NR>3' > RedistributedDemurrageToken.json
$(SOLC) RedistributedDemurrageToken.sol --bin --evm-version byzantium | awk 'NR>3' > RedistributedDemurrageToken.bin
truncate -s -1 RedistributedDemurrageToken.bin
all: multi single
multi_nocap:
$(SOLC) DemurrageTokenMultiNocap.sol --abi --evm-version byzantium | awk 'NR>3' > DemurrageTokenMultiNocap.json
$(SOLC) DemurrageTokenMultiNocap.sol --bin --evm-version byzantium | awk 'NR>3' > DemurrageTokenMultiNocap.bin
truncate -s -1 DemurrageTokenMultiNocap.bin
multi_cap:
$(SOLC) DemurrageTokenMultiCap.sol --abi --evm-version byzantium | awk 'NR>3' > DemurrageTokenMultiCap.json
$(SOLC) DemurrageTokenMultiCap.sol --bin --evm-version byzantium | awk 'NR>3' > DemurrageTokenMultiCap.bin
truncate -s -1 DemurrageTokenMultiCap.bin
multi: multi_nocap multi_cap
single_nocap:
$(SOLC) DemurrageTokenSingleNocap.sol --abi --evm-version byzantium | awk 'NR>3' > DemurrageTokenSingleNocap.json
$(SOLC) DemurrageTokenSingleNocap.sol --bin --evm-version byzantium | awk 'NR>3' > DemurrageTokenSingleNocap.bin
truncate -s -1 DemurrageTokenSingleNocap.bin
single_cap:
$(SOLC) DemurrageTokenSingleCap.sol --abi --evm-version byzantium | awk 'NR>3' > DemurrageTokenSingleCap.json
$(SOLC) DemurrageTokenSingleCap.sol --bin --evm-version byzantium | awk 'NR>3' > DemurrageTokenSingleCap.bin
truncate -s -1 DemurrageTokenSingleCap.bin
single: single_nocap single_cap
test: all
python ../python/tests/test_basic.py
python ../python/tests/test_period.py
python ../python/tests/test_redistribution.py
python ../python/tests/test_pure.py
install: all
cp -v RedistributedDemurrageToken.{json,bin} ../python/sarafu_token/data/
cp -v DemurrageToken*.{json,bin} ../python/erc20_demurrage_token/data/
.PHONY: test install