Cleanup, docs, benchmarks

This commit is contained in:
Louis Holbrook 2021-03-01 09:53:39 +00:00
parent 9de5e52c2f
commit 8e777aa720
14 changed files with 584 additions and 144 deletions

View File

@ -14,29 +14,26 @@
- Then the resulting balances after one tax period of those two trading would be 1080 Sarafu while the remaining non-active users would be 980 Sarafu. If this behaviour continued in the next tax period, with the same two users only trading (with no net balance changes), they would have 1158.39999968 Sarafu and those users that are not trading would have their balances further reduced to 960.40 Sarafu. If this continued on ~forever those two active trading users would have the entire token supply and the non-trading users would eventually reach a zero balance. - Then the resulting balances after one tax period of those two trading would be 1080 Sarafu while the remaining non-active users would be 980 Sarafu. If this behaviour continued in the next tax period, with the same two users only trading (with no net balance changes), they would have 1158.39999968 Sarafu and those users that are not trading would have their balances further reduced to 960.40 Sarafu. If this continued on ~forever those two active trading users would have the entire token supply and the non-trading users would eventually reach a zero balance.
- this example calculation for 3 tax periods can be found here: https://gitlab.com/grassrootseconomics/cic-docs/-/blob/master/demurrage-redist-sarafu.ods - this example calculation for 3 tax periods can be found here: https://gitlab.com/grassrootseconomics/cic-docs/-/blob/master/demurrage-redist-sarafu.ods
## Nomenclature
## Variables * `Demurrage` aka Decay amount: A percentage of token supply that will be charged once per minute and evenly redistributed to _active_ users every Demurrage Period (minutes)
* Base balance: The inflated balance of each user is stored for bookkeeping.
* Inputs to Constructor (Set only once during contract deployment can't be changed ) * Sink Token Address: Rounding errors and if no one trades the tax goes to this address
* `Demurrage` aka Decay amount: A percentage of token supply that will be charged once per minute and evenly redistributed to _active_ users every Demurrage Period (minutes) * Demurrage Period (minutes)- aka `period`: The number of minutes over which a user must be _active_ to receive tax-redistibution.
* Demurrage Period (minutes)- aka `period`: The number of minutes over which a user must be _active_ to receive tax-redistibution.
* Inflated Balance: The inflated balance of each user is stored for bookkeeping.
* Number of Decimals: Resolution on token (TODO) (Default 6)
* Minimum Activity Volume: (TODO) the minimum transaction amount to be considered active
* Sink Token Address: Rounding errors and if no one trades the tax goes to this address
## Ownership ## Ownership
* Contract creator is owner * Contract creator is owner
* Ownership can be transferred (also to ownership void contract "no more changes can be made") * Ownership can be transferred
## Mint ## Mint
* Owner can add minters * Owner can add minters and remove
- A faucet contract would be a minter and choose the amount of tokens to mint and distribute to new _validated_ users. - A faucet contract would be a minter and choose the amount of tokens to mint and distribute to new _validated_ users.
- The interface says the amount and is at the caller's discretion per contract call. _validation_ is outside of this contract. - The interface says the amount and is at the caller's discretion per contract call. _validation_ is outside of this contract.
* A minter can remove itself
* Minters can mint any amount * Minters can mint any amount
@ -49,7 +46,6 @@
- e.g. a `demurrage` after the 2nd minute would be give a `demurrageModifier = (1-0.02)^2 = 0.9604`. - e.g. a `demurrage` after the 2nd minute would be give a `demurrageModifier = (1-0.02)^2 = 0.9604`.
* All client-facing values (_balance output_ , _transfer inputs_) are adjusted with `demurrageModifier`. * All client-facing values (_balance output_ , _transfer inputs_) are adjusted with `demurrageModifier`.
- e.g. `_balance output_ = user_balance - user_balance * demurrageModifier` - e.g. `_balance output_ = user_balance - user_balance * demurrageModifier`
* Edge case: `approve` call, which may be called on either side of a period.
## Redistribution ## Redistribution
@ -62,27 +58,36 @@
- Check if user has participated in `period`. (_active_ user heartbeat) - Check if user has participated in `period`. (_active_ user heartbeat)
- Each _active_ user balance in the `period` is increased by `(total supply at end of period * demurrageModifier ) / number_of_active_participants` via minting - Each _active_ user balance in the `period` is increased by `(total supply at end of period * demurrageModifier ) / number_of_active_participants` via minting
- Participation field is zeroed out for that user. - Participation field is zeroed out for that user.
* Fractions must be rounded down (TODO) * Fractions must be rounded down
- Remainder is "dust" and should be sent to a dedicated "sink" token address (TODO) - Remainder is "dust" and should be sent to a dedicated Sink Token Address.
- If no one is _active_ all taxes go to the same sink address - If no one is _active_ all taxes go to the Sink Token Address.
## Data structures ## Data structures
* One word per account: * One word per `account`:
- bits 000-159: value - bits 000-071: value
- bits 160-255: period - bits 072-103: period
- (we have more room here in case we want to cram something else in) - bits 104-255: (Unused)
* One word per redistribution period: * One word per `redistributions` period:
- bits 000-055: period - bits 000-031: period
- bits 056-215: supply - bits 032-103: supply
- bits 216-253: participant count - bits 104-139: participant count
- bits 254: Set if individual redistribution amounts are fractions (TODO) - bits 140-159: demurrage modifier
- bits 255: Set if "dust" has been transferred to sink (TODO) - bits 160-254: (Unused)
- bits 255: Set if individual redistribution amounts are fractions
### Notes
Accumulated demurrage modifier in `demurrageModifier` is 128 bit, but will be _truncated_ do 20 bits in `redistributions`. The 128 bit resolution is to used to reduce the impact of fractional drift of the long-term accumulation of the demurrage modifier. However, the demurrage snapshot values used in `redistributions` are parts-per-million and can be fully contained within a 20-bit value.
## QA ## QA
* Basic python tests in place * Basic python tests in place
* How to determine and generate test vectors, and how to adapt them to scripts. * How to determine and generate sufficient test vectors, and how to adapt them to scripts.
* Audit sources? * Audit sources?
## Known issues
* A `transferFrom` following an `approve` call, when called across period thresholds, may fail if margin to demurraged amount is insufficient.

1
python/MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include sarafu_token/data/*

2
python/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
chainlib~=0.0.1a7
crypto-dev-signer~=0.4.13rc2

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,133 @@
"""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()

41
python/setup.cfg Normal file
View File

@ -0,0 +1,41 @@
[metadata]
name = sarafu-token
version = 0.0.1a2
description = ERC20 token with redistributed continual demurrage
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/grassrootseconomics/sarafu-token
keywords =
ethereum
classifiers =
Programming Language :: Python :: 3
Operating System :: OS Independent
Development Status :: 3 - Alpha
Environment :: No Input/Output (Daemon)
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Topic :: Internet
#Topic :: Blockchain :: EVM
license = GPL3
licence_files =
LICENSE
[options]
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
[options.package_data]
* =
data/RedistributedDemurrageToken.bin
data/RedistributedDemurrageToken.json
[options.entry_points]
console_scripts =
sarafu-token-deploy = sarafu_faucet.runnable.legacy.deploy:main

10
python/setup.py Normal file
View File

@ -0,0 +1,10 @@
from setuptools import setup
setup(
package_data={
'': [
'data/MintableFactor.bin',
],
},
include_package_data=True,
)

View File

@ -47,6 +47,14 @@ class Test(unittest.TestCase):
pass pass
def test_construct(self):
period = 10
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)
print('construct: {}'.format(r['gasUsed']))
def test_gas_changeperiod(self): def test_gas_changeperiod(self):
period = 43200 period = 43200
for i in range(5): for i in range(5):
@ -64,7 +72,155 @@ class Test(unittest.TestCase):
tx_hash = contract.functions.changePeriod().transact() tx_hash = contract.functions.changePeriod().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash) r = self.w3.eth.getTransactionReceipt(tx_hash)
print('{} ({}): {}'.format(i, 60 * (10 ** i), r['gasUsed'])) print('changePeriod {} ({}): {}'.format(i, 60 * (10 ** i), r['gasUsed']))
def test_mint(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
tx_hash = contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
print ('mintTo: {}'.format(r['gasUsed']))
def test_transfer(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact({'from': self.w3.eth.accounts[0]})
tx_hash = contract.functions.transfer(self.w3.eth.accounts[2], 1000000).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
print ('transfer: {}'.format(r['gasUsed']))
def test_approve(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact({'from': self.w3.eth.accounts[0]})
tx_hash = contract.functions.approve(self.w3.eth.accounts[2], 1000000).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
print ('approve: {}'.format(r['gasUsed']))
def test_transferfrom(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact({'from': self.w3.eth.accounts[0]})
contract.functions.approve(self.w3.eth.accounts[2], 1000000).transact({'from': self.w3.eth.accounts[1]})
tx_hash = contract.functions.transferFrom(self.w3.eth.accounts[1], self.w3.eth.accounts[3], 1000000).transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
print ('transferFrom: {}'.format(r['gasUsed']))
def test_redistribute_default(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
for i in range(100):
addr = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
contract.functions.mintTo(addr, 1000000 * (i+1)).transact({'from': self.w3.eth.accounts[0]})
self.eth_tester.time_travel(start_time + period * 60 + 1)
redistribution = contract.functions.redistributions(0).call()
tx_hash = contract.functions.changePeriod().transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
print ('chainPeriod -> defaultRedistribution: {}'.format(r['gasUsed']))
def test_redistribution_account(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact({'from': self.w3.eth.accounts[0]})
contract.functions.transfer(self.w3.eth.accounts[2], 1000000).transact({'from': self.w3.eth.accounts[1]})
for i in range(100):
addr = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
contract.functions.mintTo(addr, 1000000 * (i+1)).transact({'from': self.w3.eth.accounts[0]})
self.eth_tester.time_travel(start_time + period * 60 + 1)
redistribution = contract.functions.redistributions(0).call()
tx_hash = contract.functions.applyRedistributionOnAccount(self.w3.eth.accounts[1]).transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.logs[0].topics[0].hex(), '0x9a2a887706623ad3ff7fc85652deeceabe9fe1e00466c597972079ee91ea40d3')
print ('redistribute account: {}'.format(r['gasUsed']))
def test_redistribution_account_transfer(self):
period = 10
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
start_block = self.w3.eth.blockNumber
b = self.w3.eth.getBlock(start_block)
start_time = b['timestamp']
contract.functions.mintTo(self.w3.eth.accounts[1], 2000000).transact({'from': self.w3.eth.accounts[0]})
contract.functions.transfer(self.w3.eth.accounts[2], 1000000).transact({'from': self.w3.eth.accounts[1]})
for i in range(10):
addr = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
contract.functions.mintTo(addr, 1000000 * (i+1)).transact({'from': self.w3.eth.accounts[0]})
self.eth_tester.time_travel(start_time + period * 60 + 1)
redistribution = contract.functions.redistributions(0).call()
contract.functions.changePeriod().transact({'from': self.w3.eth.accounts[0]})
tx_hash = contract.functions.transfer(self.w3.eth.accounts[3], 100000).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.logs[0].topics[0].hex(), '0x9a2a887706623ad3ff7fc85652deeceabe9fe1e00466c597972079ee91ea40d3')
print ('redistribute account: {}'.format(r['gasUsed']))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -71,25 +71,30 @@ class Test(unittest.TestCase):
self.assertEqual(self.contract.functions.actualPeriod().call(), 2) self.assertEqual(self.contract.functions.actualPeriod().call(), 2)
def test_apply_demurrage(self): def test_apply_demurrage(self):
modifier = 10 * (10 ** 37) modifier = 10 * (10 ** 37)
demurrage_modifier = self.contract.functions.demurrageModifier().call() #demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_modifier &= (1 << 128) - 1 #demurrage_modifier &= (1 << 128) - 1
self.assertEqual(modifier, demurrage_modifier) demurrage_amount = self.contract.functions.demurrageAmount().call()
#self.assertEqual(modifier, demurrage_modifier)
self.assertEqual(modifier, demurrage_amount)
self.eth_tester.time_travel(self.start_time + 59) self.eth_tester.time_travel(self.start_time + 59)
demurrage_modifier = self.contract.functions.demurrageModifier().call() #demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_modifier &= (1 << 128) - 1 demurrage_amount = self.contract.functions.demurrageAmount().call()
self.assertEqual(modifier, demurrage_modifier) #demurrage_modifier &= (1 << 128) - 1
#self.assertEqual(modifier, demurrage_modifier)
self.assertEqual(modifier, demurrage_amount)
self.eth_tester.time_travel(self.start_time + 61) self.eth_tester.time_travel(self.start_time + 61)
tx_hash = self.contract.functions.applyDemurrage().transact() tx_hash = self.contract.functions.applyDemurrage().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash) r = self.w3.eth.getTransactionReceipt(tx_hash)
demurrage_modifier = self.contract.functions.demurrageModifier().call() #demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_modifier &= (1 << 128) - 1 demurrage_amount = self.contract.functions.demurrageAmount().call()
self.assertEqual(int(98 * (10 ** 36)), demurrage_modifier) #demurrage_modifier &= (1 << 128) - 1
#self.assertEqual(int(98 * (10 ** 36)), demurrage_modifier)
self.assertEqual(int(98 * (10 ** 36)), demurrage_amount)
def test_mint(self): def test_mint(self):
@ -112,6 +117,33 @@ class Test(unittest.TestCase):
self.assertEqual(balance, int(2000 * 0.98)) 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]})
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)
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]})
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[2]})
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]})
def test_base_amount(self): def test_base_amount(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact() tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash) r = self.w3.eth.getTransactionReceipt(tx_hash)
@ -120,9 +152,9 @@ class Test(unittest.TestCase):
self.eth_tester.time_travel(self.start_time + 61) self.eth_tester.time_travel(self.start_time + 61)
self.contract.functions.applyDemurrage().transact() self.contract.functions.applyDemurrage().transact()
demurrage_modifier = self.contract.functions.demurrageModifier().call() #demurrage_modifier = self.contract.functions.demurrageModifier().call()
demurrage_amount = self.contract.functions.toDemurrageAmount(demurrage_modifier).call() #demurrage_amount = self.contract.functions.toDemurrageAmount(demurrage_modifier).call()
logg.debug('d {} {}'.format(demurrage_modifier.to_bytes(32, 'big').hex(), demurrage_amount)) demurrage_amount = self.contract.functions.demurrageAmount().call()
a = self.contract.functions.toBaseAmount(1000).call(); a = self.contract.functions.toBaseAmount(1000).call();
self.assertEqual(a, 1020) self.assertEqual(a, 1020)

View File

@ -60,20 +60,21 @@ class Test(unittest.TestCase):
pass pass
@unittest.skip('this function has been removed from contract')
def test_tax_period(self): def test_tax_period(self):
t = self.contract.functions.taxLevel().call() t = self.contract.functions.taxLevel().call()
logg.debug('taxlevel {}'.format(t)) logg.debug('taxlevel {}'.format(t))
a = self.contract.functions.toTaxPeriodAmount(1000000, 0).call() a = self.contract.functions.toDemurrageAmount(1000000, 0).call()
self.assertEqual(a, 1000000) self.assertEqual(a, 1000000)
a = self.contract.functions.toTaxPeriodAmount(1000000, 1).call() a = self.contract.functions.toDemurrageAmount(1000000, 1).call()
self.assertEqual(a, 980000) self.assertEqual(a, 980000)
a = self.contract.functions.toTaxPeriodAmount(1000000, 2).call() a = self.contract.functions.toDemurrageAmount(1000000, 2).call()
self.assertEqual(a, 960400) self.assertEqual(a, 960400)
a = self.contract.functions.toTaxPeriodAmount(980000, 1).call() a = self.contract.functions.toDemurrageAmount(980000, 1).call()
self.assertEqual(a, 960400) self.assertEqual(a, 960400)

View File

@ -71,6 +71,9 @@ class Test(unittest.TestCase):
# TODO: check receipt log outputs # TODO: check receipt log outputs
def test_redistribution_storage(self): def test_redistribution_storage(self):
redistribution = self.contract.functions.redistributions(0).call();
self.assertEqual(redistribution.hex(), '000000000000000000000000f424000000000000000000000000000000000001')
self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact() self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact()
self.contract.functions.mintTo(self.w3.eth.accounts[2], 1000000).transact() self.contract.functions.mintTo(self.w3.eth.accounts[2], 1000000).transact()

View File

@ -11,6 +11,6 @@ test: all
python ../python/tests/test_redistribution.py python ../python/tests/test_redistribution.py
install: all install: all
cp -v RedistributedDemurrageToken.{json,bin} ../python/eth_address_declarator/data/ cp -v RedistributedDemurrageToken.{json,bin} ../python/sarafu_token/data/
.PHONY: test install .PHONY: test install

View File

@ -2,52 +2,118 @@ pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// TODO: assign bitmask values to contants
contract RedistributedDemurrageToken { contract RedistributedDemurrageToken {
// 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 public owner;
// Implements ERC20
string public name; string public name;
// Implements ERC20
string public symbol; string public symbol;
// Implements ERC20
uint256 public decimals; uint256 public decimals;
// Implements ERC20
uint256 public totalSupply; uint256 public totalSupply;
// Minimum amount of (demurraged) tokens an account must spend to participate in redistribution for a particular period
uint256 public minimumParticipantSpend; uint256 public minimumParticipantSpend;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
uint256 constant ppmDivider = 100000000000000000000000000000000; uint256 constant ppmDivider = 100000000000000000000000000000000;
uint256 public immutable periodStart; // timestamp // Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodDuration; // duration in SECONDS uint256 public immutable periodStart;
uint256 public immutable taxLevel; // PPM per MINUTE
uint256 public demurrageModifier; // PPM uint128(block) | uint128(ppm)
//bytes32[] public redistributions; // uint1(isFractional) | uint1(unused) | uint38(participants) | uint160(value) | uint56(period) // Duration of a single redistribution period in seconds
bytes32[] public redistributions; // uint1(isFractional) | uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period) uint256 public immutable periodDuration;
//mapping (address => bytes32) account; // uint20(unused) | uint56(period) | uint160(value)
mapping (address => bytes32) account; // uint152(unused) | uint32(period) | uint72(value) // Demurrage in ppm per minute
uint256 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter; mapping (address => bool) minter;
// Storage for ERC20 approve/transferFrom methods
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage) mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
address sinkAddress; // receives redistribuion remainders // Address to send unallocated redistribution tokens
address sinkAddress;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value); event Transfer(address indexed _from, address indexed _to, uint256 _value);
// Implements ERC20
event Approval(address indexed _owner, address indexed _spender, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// New tokens minted
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value); event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
event Debug(bytes32 _foo);
// New demurrage cache milestone calculated
event Decayed(uint256 indexed _period, uint256 indexed _periodCount, uint256 indexed _oldAmount, uint256 _newAmount); 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); event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value);
// Temporary event used in development, will be removed on prod
event Debug(bytes32 _foo);
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress) public { constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _taxLevelMinute, uint256 _periodMinutes, address _defaultSinkAddress) public {
// ACL setup
owner = msg.sender; owner = msg.sender;
minter[owner] = true; minter[owner] = true;
periodStart = block.timestamp;
periodDuration = _periodMinutes * 60; // ERC20 setup
name = _name; name = _name;
symbol = _symbol; symbol = _symbol;
decimals = _decimals; decimals = _decimals;
demurrageModifier = ppmDivider * 1000000; // Emulates 38 decimal places
demurrageModifier |= (1 << 128); // Demurrage setup
taxLevel = _taxLevelMinute; // 38 decimal places periodStart = block.timestamp;
sinkAddress = _defaultSinkAddress; 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); bytes32 initialRedistribution = toRedistribution(0, 1000000, 0, 1);
redistributions.push(initialRedistribution); redistributions.push(initialRedistribution);
// Misc settings
sinkAddress = _defaultSinkAddress;
minimumParticipantSpend = 10 ** uint256(_decimals); minimumParticipantSpend = 10 ** uint256(_decimals);
} }
@ -58,29 +124,31 @@ contract RedistributedDemurrageToken {
return true; return true;
} }
/// ERC20 // 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) { function balanceOf(address _account) public view returns (uint256) {
uint256 baseBalance; uint256 baseBalance;
uint256 anchorDemurrageAmount; uint256 currentDemurragedAmount;
uint256 anchorDemurragePeriod;
uint256 currentDemurrageAmount;
uint256 periodCount; uint256 periodCount;
baseBalance = getBaseBalance(_account); baseBalance = baseBalanceOf(_account);
anchorDemurrageAmount = toDemurrageAmount(demurrageModifier);
anchorDemurragePeriod = toDemurragePeriod(demurrageModifier);
periodCount = actualPeriod() - toDemurragePeriod(demurrageModifier); periodCount = actualPeriod() - demurragePeriod;
currentDemurrageAmount = decayBy(anchorDemurrageAmount, periodCount); currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
return (baseBalance * currentDemurrageAmount) / (ppmDivider * 1000000); return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
} }
/// Balance unmodified by demurrage /// Balance unmodified by demurrage
function getBaseBalance(address _account) private view returns (uint256) { function baseBalanceOf(address _account) public view returns (uint256) {
//return uint256(account[_account]) & 0x00ffffffffffffffffffffffffffffffffffffffff; return uint256(account[_account]) & maskAccountValue;
return uint256(account[_account]) & 0xffffffffffffffffff;
} }
/// Increases base balance for a single account /// Increases base balance for a single account
@ -89,20 +157,17 @@ contract RedistributedDemurrageToken {
uint256 newBalance; uint256 newBalance;
uint256 workAccount; uint256 workAccount;
workAccount = uint256(account[_account]); // | (newBalance & 0xffffffffffffffffff); workAccount = uint256(account[_account]);
if (_delta == 0) { if (_delta == 0) {
return false; return false;
} }
oldBalance = getBaseBalance(_account); oldBalance = baseBalanceOf(_account);
newBalance = oldBalance + _delta; newBalance = oldBalance + _delta;
require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value
//account[_account] &= bytes32(0xfffffffffffffffffffffff0000000000000000000000000000000000000000); workAccount &= (~maskAccountValue);
//account[_account] = bytes32(uint256(account[_account]) & 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000); workAccount |= (newBalance & maskAccountValue);
workAccount &= 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000;
//account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff);
workAccount |= newBalance & 0xffffffffffffffffff;
account[_account] = bytes32(workAccount); account[_account] = bytes32(workAccount);
return true; return true;
} }
@ -113,19 +178,17 @@ contract RedistributedDemurrageToken {
uint256 newBalance; uint256 newBalance;
uint256 workAccount; uint256 workAccount;
workAccount = uint256(account[_account]); // | (newBalance & 0xffffffffffffffffff); workAccount = uint256(account[_account]);
if (_delta == 0) { if (_delta == 0) {
return false; return false;
} }
oldBalance = getBaseBalance(_account); oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
newBalance = oldBalance - _delta; newBalance = oldBalance - _delta;
//account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000); workAccount &= (~maskAccountValue);
workAccount &= 0xfffffffffffffffffffffffffffffffffffffffffffff000000000000000000; workAccount |= (newBalance & maskAccountValue);
//account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff);
workAccount |= newBalance & 0xffffffffffffffffff;
account[_account] = bytes32(workAccount); account[_account] = bytes32(workAccount);
return true; return true;
} }
@ -151,31 +214,31 @@ contract RedistributedDemurrageToken {
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) { function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) {
bytes32 redistribution; bytes32 redistribution;
redistribution |= bytes32((_demurrageModifierPpm & 0x0fffff) << 140); redistribution |= bytes32((_demurrageModifierPpm << shiftRedistributionDemurrage) & maskRedistributionDemurrage);
redistribution |= bytes32((_participants & 0x0fffffffff) << 104); redistribution |= bytes32((_participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistribution |= bytes32((_value & 0xffffffffffffffffff) << 32); redistribution |= bytes32((_value << shiftRedistributionValue) & maskRedistributionValue);
redistribution |= bytes32(_period & 0xffffffff); redistribution |= bytes32(_period & maskRedistributionPeriod);
return redistribution; return redistribution;
} }
// Serializes the demurrage period part of the redistribution word // Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) { function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution) & 0xffffffff; return uint256(redistribution) & maskRedistributionPeriod;
} }
// Serializes the supply part of the redistribution word // Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) { function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x00000000000000000000000000000000000000ffffffffffffffffff00000000) >> 32; return (uint256(redistribution) & maskRedistributionValue) >> shiftRedistributionValue;
} }
// Serializes the number of participants part of the redistribution word // Serializes the number of participants part of the redistribution word
function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) { function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x00000000000000000000000000000fffffffff00000000000000000000000000) >> 104; return (uint256(redistribution) & maskRedistributionParticipants) >> shiftRedistributionParticipants;
} }
// Serializes the number of participants part of the redistribution word // Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) { function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x000000000000000000000000fffff00000000000000000000000000000000000) >> 140; return (uint256(redistribution) & maskRedistributionDemurrage) >> shiftRedistributionDemurrage;
} }
// Client accessor to the redistributions array length // Client accessor to the redistributions array length
@ -192,8 +255,8 @@ contract RedistributedDemurrageToken {
currentRedistribution = redistributions[redistributions.length-1]; currentRedistribution = redistributions[redistributions.length-1];
participants = toRedistributionParticipants(currentRedistribution) + 1; participants = toRedistributionParticipants(currentRedistribution) + 1;
tmpRedistribution = uint256(currentRedistribution); tmpRedistribution = uint256(currentRedistribution);
tmpRedistribution &= 0xfffffffffffffffffffffffffffff000000000ffffffffffffffffffffffffff; tmpRedistribution &= (~maskRedistributionParticipants);
tmpRedistribution |= (participants & 0x0fffffffff) << 104; tmpRedistribution |= ((participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
redistributions[redistributions.length-1] = bytes32(tmpRedistribution); redistributions[redistributions.length-1] = bytes32(tmpRedistribution);
@ -205,16 +268,16 @@ contract RedistributedDemurrageToken {
uint256 currentRedistribution; uint256 currentRedistribution;
currentRedistribution = uint256(redistributions[redistributions.length-1]); currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= 0xffffffffffffffffffffffffffffffffffffff000000000000000000ffffffff; currentRedistribution &= (~maskRedistributionValue);
currentRedistribution |= totalSupply << 32; currentRedistribution |= (totalSupply << shiftRedistributionValue);
redistributions[redistributions.length-1] = bytes32(currentRedistribution); redistributions[redistributions.length-1] = bytes32(currentRedistribution);
return true; return true;
} }
// Get the demurrage period of the current block number // Get the demurrage period of the current block number
function actualPeriod() public view returns (uint256) { function actualPeriod() public view returns (uint128) {
return (block.timestamp - periodStart) / periodDuration + 1; return uint128((block.timestamp - periodStart) / periodDuration + 1);
} }
// Add an entered demurrage period to the redistribution array // Add an entered demurrage period to the redistribution array
@ -232,15 +295,13 @@ contract RedistributedDemurrageToken {
// Deserialize the pemurrage period for the given account is participating in // Deserialize the pemurrage period for the given account is participating in
function accountPeriod(address _account) public view returns (uint256) { function accountPeriod(address _account) public view returns (uint256) {
//return (uint256(account[_account]) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) >> 160; return (uint256(account[_account]) & maskAccountPeriod) >> shiftAccountPeriod;
return (uint256(account[_account]) & 0x00000000000000000000000000000000000000ffffffff000000000000000000) >> 72;
} }
// Save the given demurrage period as the currently participation period for the given address // Save the given demurrage period as the currently participation period for the given address
function registerAccountPeriod(address _account, uint256 _period) private returns (bool) { function registerAccountPeriod(address _account, uint256 _period) private returns (bool) {
//account[_account] &= 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff; account[_account] &= bytes32(~maskAccountPeriod);
account[_account] &= 0xffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffff; account[_account] |= bytes32((_period << shiftAccountPeriod) & maskAccountPeriod);
account[_account] |= bytes32(_period << 72);
incrementRedistributionParticipants(); incrementRedistributionParticipants();
return true; return true;
} }
@ -277,11 +338,11 @@ contract RedistributedDemurrageToken {
if (truncatedResult < redistributionSupply) { if (truncatedResult < redistributionSupply) {
redistributionPeriod = toRedistributionPeriod(_redistribution); // since we reuse period here, can possibly be optimized by passing period instead redistributionPeriod = toRedistributionPeriod(_redistribution); // since we reuse period here, can possibly be optimized by passing period instead
redistributions[redistributionPeriod-1] &= 0xfffffffffffffffffffffffffffff000000000ffffffffffffffffffffffffff; // just to be safe, zero out all participant count data, in this case there will be only one redistributions[redistributionPeriod-1] &= bytes32(~maskRedistributionParticipants); // just to be safe, zero out all participant count data, in this case there will be only one
redistributions[redistributionPeriod-1] |= 0x8000000000000000000000000000000000000100000000000000000000000000; redistributions[redistributionPeriod-1] |= bytes32(maskRedistributionIsFractional | (1 << shiftRedistributionParticipants));
} }
increaseBaseBalance(sinkAddress, unit / ppmDivider); //truncatedResult); increaseBaseBalance(sinkAddress, unit / ppmDivider);
return unit; return unit;
} }
@ -294,8 +355,8 @@ contract RedistributedDemurrageToken {
return false; return false;
} }
// is this needed? // TODO: is this needed?
redistributions[_period-1] |= 0x8000000000000000000000000000000000000000000000000000000000000000; redistributions[_period-1] |= bytes32(maskRedistributionIsFractional);
periodSupply = toRedistributionSupply(redistributions[_period-1]); periodSupply = toRedistributionSupply(redistributions[_period-1]);
increaseBaseBalance(sinkAddress, periodSupply - _remainder); increaseBaseBalance(sinkAddress, periodSupply - _remainder);
@ -303,32 +364,22 @@ contract RedistributedDemurrageToken {
} }
function toDemurrageAmount(uint256 _demurrage) public pure returns (uint256) { // Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
return _demurrage & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff;
}
function toDemurragePeriod(uint256 _demurrage) public pure returns (uint256) {
return (_demurrage & 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000) >> 128;
}
function applyDemurrage() public returns (bool) { function applyDemurrage() public returns (bool) {
uint256 epochPeriodCount; uint128 epochPeriodCount;
uint256 periodCount; uint128 periodCount;
uint256 lastDemurrageAmount; uint256 lastDemurrageAmount;
uint256 newDemurrageAmount; uint256 newDemurrageAmount;
epochPeriodCount = actualPeriod(); epochPeriodCount = actualPeriod();
//epochPeriodCount = (block.timestamp - periodStart) / periodDuration; // toDemurrageTime(demurrageModifier); periodCount = epochPeriodCount - demurragePeriod;
periodCount = epochPeriodCount - toDemurragePeriod(demurrageModifier);
if (periodCount == 0) { if (periodCount == 0) {
return false; return false;
} }
lastDemurrageAmount = toDemurrageAmount(demurrageModifier); lastDemurrageAmount = demurrageAmount;
newDemurrageAmount = decayBy(lastDemurrageAmount, periodCount); demurrageAmount = uint128(decayBy(lastDemurrageAmount, periodCount));
demurrageModifier = 0; demurragePeriod = epochPeriodCount;
demurrageModifier |= (newDemurrageAmount & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff); emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, demurrageAmount);
demurrageModifier |= (epochPeriodCount << 128);
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, newDemurrageAmount);
return true; return true;
} }
@ -344,7 +395,6 @@ contract RedistributedDemurrageToken {
// Recalculate the demurrage modifier for the new period // 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) // After this, all REPORTED balances will have been reduced by the corresponding ratio (but the effecive totalsupply stays the same)
//function applyTax() public returns (uint256) {
function changePeriod() public returns (bool) { function changePeriod() public returns (bool) {
bytes32 currentRedistribution; bytes32 currentRedistribution;
bytes32 nextRedistribution; bytes32 nextRedistribution;
@ -355,6 +405,7 @@ contract RedistributedDemurrageToken {
uint256 nextRedistributionDemurrage; uint256 nextRedistributionDemurrage;
uint256 demurrageCounts; uint256 demurrageCounts;
uint256 periodTimestamp; uint256 periodTimestamp;
uint256 nextPeriod;
currentRedistribution = checkPeriod(); currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) { if (currentRedistribution == bytes32(0x00)) {
@ -362,10 +413,11 @@ contract RedistributedDemurrageToken {
} }
currentPeriod = toRedistributionPeriod(currentRedistribution); currentPeriod = toRedistributionPeriod(currentRedistribution);
nextPeriod = currentPeriod + 1;
periodTimestamp = getPeriodTimeDelta(currentPeriod); periodTimestamp = getPeriodTimeDelta(currentPeriod);
applyDemurrage(); applyDemurrage();
currentDemurrageAmount = toDemurrageAmount(demurrageModifier); currentDemurrageAmount = demurrageAmount;
demurrageCounts = demurrageCycles(periodTimestamp); demurrageCounts = demurrageCycles(periodTimestamp);
if (demurrageCounts > 0) { if (demurrageCounts > 0) {
@ -374,8 +426,7 @@ contract RedistributedDemurrageToken {
nextRedistributionDemurrage = currentDemurrageAmount / ppmDivider; nextRedistributionDemurrage = currentDemurrageAmount / ppmDivider;
} }
nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply, currentPeriod + 1); nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply, nextPeriod);
emit Debug(bytes32(currentDemurrageAmount));
redistributions.push(nextRedistribution); redistributions.push(nextRedistribution);
currentParticipants = toRedistributionParticipants(currentRedistribution); currentParticipants = toRedistributionParticipants(currentRedistribution);
@ -385,14 +436,15 @@ contract RedistributedDemurrageToken {
currentRemainder = remainder(currentParticipants, totalSupply); // we can use totalSupply directly because it will always be the same as the recorded supply on the current redistribution 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); applyRemainderOnPeriod(currentRemainder, currentPeriod);
} }
emit Period(nextPeriod);
return true; 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) { function growBy(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor; uint256 valueFactor;
uint256 truncatedTaxLevel; uint256 truncatedTaxLevel;
// TODO: if can't get to work, reverse the iteration from current period.
valueFactor = 1000000; valueFactor = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider; truncatedTaxLevel = taxLevel / ppmDivider;
@ -403,12 +455,11 @@ contract RedistributedDemurrageToken {
} }
// Calculate a value reduced by demurrage by the given period // Calculate a value reduced by demurrage by the given period
// TODO: higher precision // TODO: higher precision if possible
function decayBy(uint256 _value, uint256 _period) public view returns (uint256) { function decayBy(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor; uint256 valueFactor;
uint256 truncatedTaxLevel; uint256 truncatedTaxLevel;
// TODO: if can't get to work, reverse the iteration from current period.
valueFactor = 1000000; valueFactor = 1000000;
truncatedTaxLevel = taxLevel / ppmDivider; truncatedTaxLevel = taxLevel / ppmDivider;
@ -427,6 +478,7 @@ contract RedistributedDemurrageToken {
uint256 baseValue; uint256 baseValue;
uint256 value; uint256 value;
uint256 period; uint256 period;
uint256 demurrage;
period = accountPeriod(_account); period = accountPeriod(_account);
if (period == 0 || period >= actualPeriod()) { if (period == 0 || period >= actualPeriod()) {
@ -439,11 +491,12 @@ contract RedistributedDemurrageToken {
} }
supply = toRedistributionSupply(periodRedistribution); supply = toRedistributionSupply(periodRedistribution);
demurrage = toRedistributionDemurrageModifier(periodRedistribution);
baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider; baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider;
value = decayBy(baseValue, period - 1); value = (baseValue * demurrage) / 1000000;
//account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff); // zero out period for the account
account[_account] &= bytes32(0xffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffff); account[_account] &= bytes32(~maskAccountPeriod);
increaseBaseBalance(_account, value); increaseBaseBalance(_account, value);
emit Redistribution(_account, period, value); emit Redistribution(_account, period, value);
@ -452,7 +505,8 @@ contract RedistributedDemurrageToken {
// Inflates the given amount according to the current demurrage modifier // Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) { function toBaseAmount(uint256 _value) public view returns (uint256) {
return (_value * ppmDivider * 1000000) / toDemurrageAmount(demurrageModifier); //return (_value * ppmDivider * 1000000) / toDemurrageAmount(demurrageModifier);
return (_value * ppmDivider * 1000000) / demurrageAmount;
} }
// ERC20, triggers tax and/or redistribution // ERC20, triggers tax and/or redistribution
@ -476,10 +530,9 @@ contract RedistributedDemurrageToken {
changePeriod(); changePeriod();
applyRedistributionOnAccount(msg.sender); applyRedistributionOnAccount(msg.sender);
// TODO: Prefer to truncate the result, instead it seems to round to nearest :/
baseValue = toBaseAmount(_value); baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue); result = transferBase(msg.sender, _to, baseValue);
emit Transfer(msg.sender, _to, _value);
return result; return result;
} }
@ -496,6 +549,7 @@ contract RedistributedDemurrageToken {
require(allowance[_from][msg.sender] >= baseValue); require(allowance[_from][msg.sender] >= baseValue);
result = transferBase(_from, _to, baseValue); result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result; return result;
} }