Compare commits
20 Commits
lash/use-t
...
lash/clean
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a168d81f9 | ||
|
|
2f57da0e8e
|
||
|
|
51c9c94261
|
||
|
|
e332f76a04
|
||
|
|
ebef1948aa
|
||
|
|
22cd7cf18c
|
||
|
|
7a9572e978
|
||
|
|
e2ecc6d382 | ||
|
|
8e777aa720 | ||
|
|
8f11bdc2cc
|
||
|
|
aab0bc243c
|
||
|
|
364731b220
|
||
|
|
a8ff826dad
|
||
|
|
3ae75075e4
|
||
|
|
9de5e52c2f | ||
|
|
528fef6444 | ||
| 8af12b33c0 | |||
| e32001e7e0 | |||
|
|
0f816ebdc5 | ||
|
|
0202676d51 |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
build/
|
||||
dist/
|
||||
*.egg-info
|
||||
__pycache__
|
||||
*.pyc
|
||||
gmon.out
|
||||
solidity/*.json
|
||||
solidity/*.bin
|
||||
85
README.md
85
README.md
@@ -3,86 +3,91 @@
|
||||
## Use Case
|
||||
* Network / Basic Income Token
|
||||
* 100 Sarafu is distributed to anyone in Kenya after user validation by the owner of a faucet which mints new Sarafu.
|
||||
* Validated users are those that validate their phone number in Kenya.
|
||||
* A monthly Sarafu holding tax aka ([demurrage](https://en.wikipedia.org/wiki/Demurrage_(currency))) of 2% is deducted from users
|
||||
* Each month (after a number of blocks) the total amount tax is distributed evenly out to _active_ users.
|
||||
* any single transaction by a user is considered _active_ (heartbeat) (possibly add minimum size of heartbeat in constructor (TODO))
|
||||
* Validated users are those that validate their phone number in Kenya.
|
||||
* A Sarafu holding tax aka ([demurrage](https://en.wikipedia.org/wiki/Demurrage_(currency))) of 0.000050105908373373% is charged from users per minute - such that over 1 month to total tax would be 2%.
|
||||
* After 1 week the total amount tax is distributed evenly out to _active_ users.
|
||||
* any single transaction by a user within that week is considered _active_ (heartbeat)
|
||||
* This is meant to result in a disincentivization to hold (hodl) the Sarafu token and increase its usage as a medium of exchange rather than a store of value.
|
||||
* This token can be added to liquidity pools with other ERC20 tokens and or Community Inclusion Currencies (CICs) - and thereby act as a central network token and connect various tokens and CICs together.
|
||||
* Example
|
||||
- With a demurrage of 2% - If there are 10 users all with balances of 1000 Sarafu and only 2 of them trade (assume they trade back and forth with no net balance change).
|
||||
- With a demurrage of 2% (net per month) and a reward period of 1 month - If there are 10 users all with balances of 1000 Sarafu and only 2 of them trade that month (assume they trade back and forth with no net balance change).
|
||||
- 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
|
||||
|
||||
## Nomenclature
|
||||
|
||||
## Variables
|
||||
|
||||
* Inputs to Constructor (Set only once during contract deployment can't be changed )
|
||||
* `Demurrage` aka Decay amount: A percentage of token supply that will be charged once per - aka `period` and evenly redistributed to _active_ users
|
||||
* Demurrage Period (blocks)- aka `period`: The number of blocks (equivalent to a time frame) over which a new Holding Fee is applied and redistributed.
|
||||
* 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
|
||||
* `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.
|
||||
* Sink Token Address: Rounding errors and if no one trades the tax goes to this address
|
||||
* Demurrage Period (minutes)- aka `period`: The number of minutes over which a user must be _active_ to receive tax-redistibution.
|
||||
|
||||
|
||||
## Ownership
|
||||
|
||||
* Contract creator is owner
|
||||
* Ownership can be transferred (also to ownership void contract "no more changes can be made")
|
||||
* Ownership can be transferred
|
||||
|
||||
|
||||
## 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.
|
||||
- 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
|
||||
|
||||
|
||||
## Demurrage
|
||||
* Holding Tax (`demurrage`) is applied when a **mint** or **transfer** is triggered for first time/block in a new `period`; (it can also be triggered explicitly)
|
||||
- Supply _stays the same_.
|
||||
- Updates `demurrageModifier` which represents the accumulated tax value and is an exponential decay step (of size `demurrage`) for each `period`
|
||||
- `demurrageModifier = (1-demurrage)^period`
|
||||
- e.g. a `demurrage` of 2% at a `period` of 1 would be give a `demurrageModifier = (1-0.02)^1 = 0.98`.
|
||||
- e.g. a `demurrage` of 2% at a `period` of 2 would be give a `demurrageModifier = (1-0.02)^2 = 0.9604`.
|
||||
* Holding Tax (`demurrage`) is applied when a **mint** or **transfer**; (it can also be triggered explicitly)
|
||||
- Note that the token supply _stays the same_ but a virtual _balance output_ is created.
|
||||
- Updates `demurrageModifier` which represents the accumulated tax value and is an exponential decay step (of size `demurrage`) for each minute that has passed.
|
||||
- `demurrageModifier = (1-demurrage)^(minute_passed)`
|
||||
- e.g. a `demurrage` of 2% after the 1st minute would be give a `demurrageModifier = (1-0.02)^1 = 0.98`.
|
||||
- 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`.
|
||||
- e.g. `_balance output_ = user_balance - user_balance * demurrageModifier`
|
||||
* Edge case: `approve` call, which may be called on either side of a period.
|
||||
|
||||
|
||||
## Redistribution
|
||||
|
||||
* One redistribution entry is added to storage for each period;
|
||||
* One redistribution entry is added to storage for each `period`;
|
||||
- When `mint` is triggered, the new totalsupply is stored to the entry
|
||||
- When `transfer` is triggered, and the account did not yet participate in the `period`, the entry's participant count is incremented.
|
||||
* Account must have "participated" in a period to be redistribution beneficiary.
|
||||
* Redistribution is applied when an account triggers a **transfer** for the first time in a new period;
|
||||
* Redistribution is applied when an account triggers a **transfer** for the first time in a new `period`;
|
||||
- Check if user has participated in `period`. (_active_ user heartbeat)
|
||||
- Each _active_ user balance 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.
|
||||
* Fractions must be rounded down (TODO)
|
||||
- Remainder is "dust" and should be sent to a dedicated "sink" token address (TODO)
|
||||
- If no one is _active_ all taxes go to the same sink address
|
||||
* Fractions must be rounded down
|
||||
- Remainder is "dust" and should be sent to a dedicated Sink Token Address.
|
||||
- If no one is _active_ all taxes go to the Sink Token Address.
|
||||
|
||||
|
||||
## Data structures
|
||||
|
||||
* One word per account:
|
||||
- bits 000-159: value
|
||||
- bits 160-255: period
|
||||
- (we have more room here in case we want to cram something else in)
|
||||
* One word per redistribution period:
|
||||
- bits 000-055: period
|
||||
- bits 056-215: supply
|
||||
- bits 216-253: participant count
|
||||
- bits 254: Set if individual redistribution amounts are fractions (TODO)
|
||||
- bits 255: Set if "dust" has been transferred to sink (TODO)
|
||||
* One word per `account`:
|
||||
- bits 000-071: value
|
||||
- bits 072-103: period
|
||||
- bits 104-255: (Unused)
|
||||
* One word per `redistributions` period:
|
||||
- bits 000-031: period
|
||||
- bits 032-103: supply
|
||||
- bits 104-139: participant count
|
||||
- bits 140-159: demurrage modifier
|
||||
- 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
|
||||
|
||||
* 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?
|
||||
|
||||
## 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
1
python/MANIFEST.in
Normal file
@@ -0,0 +1 @@
|
||||
include sarafu_token/data/*
|
||||
3
python/requirements.txt
Normal file
3
python/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
chainlib~=0.0.3a1
|
||||
eth-erc20~=0.0.9a1
|
||||
crypto-dev-signer~=0.4.14b3
|
||||
1
python/sarafu_token/__init__.py
Normal file
1
python/sarafu_token/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .token import RedistributedDemurrageToken
|
||||
1
python/sarafu_token/data/RedistributedDemurrageToken.bin
Normal file
1
python/sarafu_token/data/RedistributedDemurrageToken.bin
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
python/sarafu_token/data/__init__.py
Normal file
3
python/sarafu_token/data/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
|
||||
data_dir = os.path.realpath(os.path.dirname(__file__))
|
||||
135
python/sarafu_token/runnable/deploy.py
Normal file
135
python/sarafu_token/runnable/deploy.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Deploy sarafu token
|
||||
|
||||
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
|
||||
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
|
||||
|
||||
"""
|
||||
|
||||
# standard imports
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
|
||||
from crypto_dev_signer.keystore.dict import DictKeystore
|
||||
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 sarafu_token import RedistributedDemurrageToken
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logg = logging.getLogger()
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
data_dir = os.path.join(script_dir, '..', 'data')
|
||||
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', type=str, help='Web3 provider url (http only)')
|
||||
argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
|
||||
argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
|
||||
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
|
||||
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
|
||||
argparser.add_argument('-v', action='store_true', help='Be verbose')
|
||||
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
|
||||
argparser.add_argument('-d', action='store_true', help='Dump RPC calls to terminal and do not send')
|
||||
argparser.add_argument('--name', type=str, help='Token name')
|
||||
argparser.add_argument('--decimals', default=6, type=int, help='Token decimals')
|
||||
argparser.add_argument('--gas-price', type=int, dest='gas_price', help='Override gas price')
|
||||
argparser.add_argument('--nonce', type=int, help='Override transaction nonce')
|
||||
argparser.add_argument('--sink-address', default=ZERO_ADDRESS, type=str, help='demurrage level,ppm per minute')
|
||||
argparser.add_argument('--redistribution-period', default=10080, type=int, help='redistribution period, minutes') # default 10080 = week
|
||||
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
|
||||
argparser.add_argument('symbol', default='SRF', type=str, help='Token symbol')
|
||||
argparser.add_argument('demurrage_level', type=int, help='demurrage level, ppm per minute')
|
||||
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
|
||||
|
||||
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=RedistributedDemurrageToken.gas)
|
||||
else:
|
||||
gas_oracle = RPCGasOracle(rpc, code_callback=RedistributedDemurrageToken.gas)
|
||||
|
||||
dummy = args.d
|
||||
|
||||
token_name = args.name
|
||||
if token_name == None:
|
||||
token_name = args.symbol
|
||||
|
||||
def main():
|
||||
c = RedistributedDemurrageToken(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
|
||||
(tx_hash_hex, o) = c.constructor(
|
||||
signer_address,
|
||||
token_name,
|
||||
args.symbol,
|
||||
args.decimals,
|
||||
args.demurrage_level,
|
||||
args.redistribution_period,
|
||||
args.sink_address,
|
||||
)
|
||||
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()
|
||||
133
python/sarafu_token/runnable/legacy/deploy.py
Normal file
133
python/sarafu_token/runnable/legacy/deploy.py
Normal 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()
|
||||
87
python/sarafu_token/token.py
Normal file
87
python/sarafu_token/token.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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,
|
||||
)
|
||||
|
||||
# local imports
|
||||
from sarafu_token.data import data_dir
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RedistributedDemurrageToken(TxFactory):
|
||||
|
||||
__abi = None
|
||||
__bytecode = None
|
||||
|
||||
def constructor(self, sender_address, name, symbol, decimals, demurrage_level, period_minutes, sink_address, tx_format=TxFormat.JSONRPC):
|
||||
code = RedistributedDemurrageToken.bytecode()
|
||||
enc = ABIContractEncoder()
|
||||
enc.string(name)
|
||||
enc.string(symbol)
|
||||
enc.uint256(decimals)
|
||||
enc.uint256(demurrage_level)
|
||||
enc.uint256(period_minutes)
|
||||
enc.address(sink_address)
|
||||
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 abi():
|
||||
if RedistributedDemurrageToken.__abi == None:
|
||||
f = open(os.path.join(data_dir, 'RedistributedDemurrageToken.json'), 'r')
|
||||
RedistributedDemurrageToken.__abi = json.load(f)
|
||||
f.close()
|
||||
return RedistributedDemurrageToken.__abi
|
||||
|
||||
|
||||
@staticmethod
|
||||
def bytecode():
|
||||
if RedistributedDemurrageToken.__bytecode == None:
|
||||
f = open(os.path.join(data_dir, 'RedistributedDemurrageToken.bin'), 'r')
|
||||
RedistributedDemurrageToken.__bytecode = f.read()
|
||||
f.close()
|
||||
return RedistributedDemurrageToken.__bytecode
|
||||
|
||||
|
||||
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 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
|
||||
41
python/setup.cfg
Normal file
41
python/setup.cfg
Normal file
@@ -0,0 +1,41 @@
|
||||
[metadata]
|
||||
name = sarafu-token
|
||||
version = 0.0.1a8
|
||||
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
|
||||
install_requires =
|
||||
chainlib~=0.0.3a1
|
||||
crypto-dev-signer~=0.4.14b3
|
||||
|
||||
|
||||
[options.package_data]
|
||||
* =
|
||||
data/RedistributedDemurrageToken.bin
|
||||
data/RedistributedDemurrageToken.json
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
sarafu-token-deploy = sarafu_token.runnable.deploy:main
|
||||
31
python/setup.py
Normal file
31
python/setup.py
Normal file
@@ -0,0 +1,31 @@
|
||||
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={
|
||||
'': [
|
||||
'data/MintableFactor.bin',
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
tests_require=test_requirements,
|
||||
)
|
||||
3
python/test_requirements.txt
Normal file
3
python/test_requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
web3==5.12.2
|
||||
eth_tester==0.5.0b3
|
||||
py-evm==0.3.0a20
|
||||
228
python/tests/bench.py
Normal file
228
python/tests/bench.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# standard imports
|
||||
import os
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import web3
|
||||
import eth_tester
|
||||
import eth_abi
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logg = logging.getLogger()
|
||||
|
||||
logging.getLogger('web3').setLevel(logging.WARNING)
|
||||
logging.getLogger('eth.vm').setLevel(logging.WARNING)
|
||||
|
||||
testdir = os.path.dirname(__file__)
|
||||
|
||||
TAX_LEVEL = 10000 * 2 # 2%
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
contract = None
|
||||
|
||||
def setUp(self):
|
||||
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
|
||||
'gas_limit': 9000000,
|
||||
})
|
||||
|
||||
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
|
||||
self.bytecode = f.read()
|
||||
f.close()
|
||||
|
||||
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
|
||||
self.abi = json.load(f)
|
||||
f.close()
|
||||
|
||||
backend = eth_tester.PyEVMBackend(eth_params)
|
||||
self.eth_tester = eth_tester.EthereumTester(backend)
|
||||
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
|
||||
self.w3 = web3.Web3(provider)
|
||||
self.sink_address = self.w3.eth.accounts[9]
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_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):
|
||||
period = 43200
|
||||
for i in range(5):
|
||||
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
|
||||
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL * (10 ** 32), period, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
|
||||
r = self.w3.eth.getTransactionReceipt(tx_hash)
|
||||
contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
|
||||
|
||||
start_block = self.w3.eth.blockNumber
|
||||
b = self.w3.eth.getBlock(start_block)
|
||||
start_time = b['timestamp']
|
||||
|
||||
period_seconds = period * 60
|
||||
self.eth_tester.time_travel(start_time + period_seconds + (60 * (10 ** i)))
|
||||
tx_hash = contract.functions.changePeriod().transact()
|
||||
r = self.w3.eth.getTransactionReceipt(tx_hash)
|
||||
|
||||
print('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__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
@@ -71,25 +71,30 @@ class Test(unittest.TestCase):
|
||||
self.assertEqual(self.contract.functions.actualPeriod().call(), 2)
|
||||
|
||||
|
||||
|
||||
def test_apply_demurrage(self):
|
||||
modifier = 10 * (10 ** 37)
|
||||
demurrage_modifier = self.contract.functions.demurrageModifier().call()
|
||||
demurrage_modifier &= (1 << 128) - 1
|
||||
self.assertEqual(modifier, demurrage_modifier)
|
||||
#demurrage_modifier = self.contract.functions.demurrageModifier().call()
|
||||
#demurrage_modifier &= (1 << 128) - 1
|
||||
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)
|
||||
demurrage_modifier = self.contract.functions.demurrageModifier().call()
|
||||
demurrage_modifier &= (1 << 128) - 1
|
||||
self.assertEqual(modifier, demurrage_modifier)
|
||||
#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.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_modifier &= (1 << 128) - 1
|
||||
self.assertEqual(int(98 * (10 ** 36)), demurrage_modifier)
|
||||
#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)
|
||||
|
||||
|
||||
def test_mint(self):
|
||||
@@ -111,7 +116,34 @@ class Test(unittest.TestCase):
|
||||
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
|
||||
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):
|
||||
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact()
|
||||
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.contract.functions.applyDemurrage().transact()
|
||||
demurrage_modifier = self.contract.functions.demurrageModifier().call()
|
||||
demurrage_amount = self.contract.functions.toDemurrageAmount(demurrage_modifier).call()
|
||||
logg.debug('d {} {}'.format(demurrage_modifier.to_bytes(32, 'big').hex(), demurrage_amount))
|
||||
#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)
|
||||
|
||||
@@ -60,20 +60,21 @@ class Test(unittest.TestCase):
|
||||
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.toTaxPeriodAmount(1000000, 0).call()
|
||||
a = self.contract.functions.toDemurrageAmount(1000000, 0).call()
|
||||
self.assertEqual(a, 1000000)
|
||||
|
||||
a = self.contract.functions.toTaxPeriodAmount(1000000, 1).call()
|
||||
a = self.contract.functions.toDemurrageAmount(1000000, 1).call()
|
||||
self.assertEqual(a, 980000)
|
||||
|
||||
a = self.contract.functions.toTaxPeriodAmount(1000000, 2).call()
|
||||
a = self.contract.functions.toDemurrageAmount(1000000, 2).call()
|
||||
self.assertEqual(a, 960400)
|
||||
|
||||
a = self.contract.functions.toTaxPeriodAmount(980000, 1).call()
|
||||
a = self.contract.functions.toDemurrageAmount(980000, 1).call()
|
||||
self.assertEqual(a, 960400)
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ class Test(unittest.TestCase):
|
||||
|
||||
# TODO: check receipt log outputs
|
||||
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[2], 1000000).transact()
|
||||
|
||||
@@ -84,14 +87,14 @@ class Test(unittest.TestCase):
|
||||
self.eth_tester.time_travel(self.start_time + 61)
|
||||
|
||||
redistribution = self.contract.functions.redistributions(0).call();
|
||||
self.assertEqual(redistribution.hex(), '000000000100000000000000000000000000000000001e848000000000000001')
|
||||
self.assertEqual(redistribution.hex(), '000000000000000000000000f42400000000010000000000001e848000000001')
|
||||
|
||||
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[0], 1000000).transact()
|
||||
r = self.w3.eth.getTransactionReceipt(tx_hash)
|
||||
self.assertEqual(r.status, 1)
|
||||
|
||||
redistribution = self.contract.functions.redistributions(1).call()
|
||||
self.assertEqual(redistribution.hex(), '000000000000000000000000000000000000000000002dc6c000000000000002')
|
||||
self.assertEqual(redistribution.hex(), '000000000000000000000000ef4200000000000000000000002dc6c000000002')
|
||||
|
||||
|
||||
def test_redistribution_balance_on_zero_participants(self):
|
||||
@@ -147,7 +150,7 @@ class Test(unittest.TestCase):
|
||||
r = self.w3.eth.getTransactionReceipt(tx_hash)
|
||||
# No cheating!
|
||||
self.contract.functions.transfer(self.w3.eth.accounts[3], spend_amount).transact({'from': self.w3.eth.accounts[3]})
|
||||
# Cheapskate!
|
||||
# No cheapskating!
|
||||
self.contract.functions.transfer(external_address, spend_amount-1).transact({'from': self.w3.eth.accounts[4]})
|
||||
|
||||
self.assertEqual(r.status, 1)
|
||||
|
||||
@@ -11,6 +11,6 @@ test: all
|
||||
python ../python/tests/test_redistribution.py
|
||||
|
||||
install: all
|
||||
cp -v RedistributedDemurrageToken.{json,bin} ../python/eth_address_declarator/data/
|
||||
cp -v RedistributedDemurrageToken.{json,bin} ../python/sarafu_token/data/
|
||||
|
||||
.PHONY: test install
|
||||
|
||||
@@ -2,50 +2,123 @@ pragma solidity > 0.6.11;
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// TODO: assign bitmask values to contants
|
||||
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 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;
|
||||
|
||||
uint256 public immutable periodStart; // timestamp
|
||||
uint256 public immutable periodDuration; // duration in SECONDS
|
||||
uint256 public immutable taxLevel; // PPM per MINUTE
|
||||
uint256 public demurrageModifier; // PPM uint128(block) | uint128(ppm)
|
||||
// Timestamp of start of periods (time which contract constructor was called)
|
||||
uint256 public immutable periodStart;
|
||||
|
||||
bytes32[] public redistributions; // uint1(isFractional) | uint1(unused) | uint38(participants) | uint160(value) | uint56(period)
|
||||
mapping (address => bytes32) account; // uint20(unused) | uint56(period) | uint160(value)
|
||||
// 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 sinkAddress; // receives redistribuion remainders
|
||||
// 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);
|
||||
//event Debug(uint256 _foo);
|
||||
|
||||
// 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;
|
||||
periodStart = block.timestamp;
|
||||
periodDuration = _periodMinutes * 60;
|
||||
|
||||
// ERC20 setup
|
||||
name = _name;
|
||||
symbol = _symbol;
|
||||
decimals = _decimals;
|
||||
demurrageModifier = ppmDivider * 1000000; // Emulates 38 decimal places
|
||||
demurrageModifier |= (1 << 128);
|
||||
taxLevel = _taxLevelMinute; // 38 decimal places
|
||||
sinkAddress = _defaultSinkAddress;
|
||||
bytes32 initialRedistribution = toRedistribution(0, 0, 1);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -56,44 +129,51 @@ contract RedistributedDemurrageToken {
|
||||
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) {
|
||||
uint256 baseBalance;
|
||||
uint256 anchorDemurrageAmount;
|
||||
uint256 anchorDemurragePeriod;
|
||||
uint256 currentDemurrageAmount;
|
||||
uint256 currentDemurragedAmount;
|
||||
uint256 periodCount;
|
||||
|
||||
baseBalance = getBaseBalance(_account);
|
||||
anchorDemurrageAmount = toDemurrageAmount(demurrageModifier);
|
||||
anchorDemurragePeriod = toDemurragePeriod(demurrageModifier);
|
||||
baseBalance = baseBalanceOf(_account);
|
||||
|
||||
periodCount = actualPeriod() - toDemurragePeriod(demurrageModifier);
|
||||
periodCount = actualPeriod() - demurragePeriod;
|
||||
|
||||
currentDemurrageAmount = toTaxPeriodAmount(anchorDemurrageAmount, periodCount);
|
||||
currentDemurragedAmount = uint128(decayBy(demurrageAmount, periodCount));
|
||||
|
||||
return (baseBalance * currentDemurrageAmount) / (ppmDivider * 1000000);
|
||||
return (baseBalance * currentDemurragedAmount) / (ppmDivider * 1000000);
|
||||
}
|
||||
|
||||
/// Balance unmodified by demurrage
|
||||
function getBaseBalance(address _account) private view returns (uint256) {
|
||||
return uint256(account[_account]) & 0x00ffffffffffffffffffffffffffffffffffffffff;
|
||||
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 = getBaseBalance(_account);
|
||||
oldBalance = baseBalanceOf(_account);
|
||||
newBalance = oldBalance + _delta;
|
||||
require(uint160(newBalance) > uint160(oldBalance), 'ERR_WOULDWRAP'); // revert if increase would result in a wrapped value
|
||||
account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000);
|
||||
account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff);
|
||||
workAccount &= (~maskAccountValue);
|
||||
workAccount |= (newBalance & maskAccountValue);
|
||||
account[_account] = bytes32(workAccount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,56 +181,69 @@ contract RedistributedDemurrageToken {
|
||||
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 = getBaseBalance(_account);
|
||||
oldBalance = baseBalanceOf(_account);
|
||||
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
|
||||
newBalance = oldBalance - _delta;
|
||||
account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000);
|
||||
account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff);
|
||||
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]);
|
||||
|
||||
applyDemurrage();
|
||||
changePeriod();
|
||||
baseAmount = _amount;
|
||||
totalSupply += _amount;
|
||||
increaseBaseBalance(_beneficiary, _amount);
|
||||
increaseBaseBalance(_beneficiary, baseAmount);
|
||||
emit Mint(msg.sender, _beneficiary, _amount);
|
||||
saveRedistributionSupply();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deserializes the redistribution word
|
||||
function toRedistribution(uint256 _participants, uint256 _value, uint256 _period) private pure returns(bytes32) {
|
||||
// uint1(isFractional) | uint95(unused) | uint20(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
|
||||
function toRedistribution(uint256 _participants, uint256 _demurrageModifierPpm, uint256 _value, uint256 _period) private pure returns(bytes32) {
|
||||
bytes32 redistribution;
|
||||
|
||||
redistribution |= bytes32((_participants & 0x7fffffffff) << 216);
|
||||
redistribution |= bytes32((_value & 0xffffffffffffffffffffffff) << 56);
|
||||
redistribution |= bytes32(_period & 0xffffffffffffff);
|
||||
redistribution |= bytes32((_demurrageModifierPpm << 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 & 0x00000000000000000000000000000000000000000000000000ffffffffffffff);
|
||||
return uint256(redistribution) & maskRedistributionPeriod;
|
||||
}
|
||||
|
||||
// Serializes the supply part of the redistribution word
|
||||
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
|
||||
return uint256(redistribution & 0x0000000000ffffffffffffffffffffffffffffffffffffffff00000000000000) >> 56;
|
||||
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 & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216;
|
||||
return (uint256(redistribution) & maskRedistributionParticipants) >> shiftRedistributionParticipants;
|
||||
}
|
||||
|
||||
// Serializes the number of participants part of the redistribution word
|
||||
function toRedistributionDemurrageModifier(bytes32 redistribution) public pure returns (uint256) {
|
||||
return (uint256(redistribution) & maskRedistributionDemurrage) >> shiftRedistributionDemurrage;
|
||||
}
|
||||
|
||||
// Client accessor to the redistributions array length
|
||||
@@ -160,16 +253,19 @@ contract RedistributedDemurrageToken {
|
||||
|
||||
// Add number of participants for the current redistribution period by one
|
||||
function incrementRedistributionParticipants() private returns (bool) {
|
||||
uint256 currentRedistribution;
|
||||
bytes32 currentRedistribution;
|
||||
uint256 tmpRedistribution;
|
||||
uint256 participants;
|
||||
|
||||
currentRedistribution = uint256(redistributions[redistributions.length-1]);
|
||||
participants = ((currentRedistribution & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216) + 1;
|
||||
currentRedistribution &= 0x8000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
||||
currentRedistribution |= participants << 216;
|
||||
currentRedistribution = redistributions[redistributions.length-1];
|
||||
participants = toRedistributionParticipants(currentRedistribution) + 1;
|
||||
tmpRedistribution = uint256(currentRedistribution);
|
||||
tmpRedistribution &= (~maskRedistributionParticipants);
|
||||
tmpRedistribution |= ((participants << shiftRedistributionParticipants) & maskRedistributionParticipants);
|
||||
|
||||
//emit Debug(participants);
|
||||
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
|
||||
redistributions[redistributions.length-1] = bytes32(tmpRedistribution);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save the current total supply amount to the current redistribution period
|
||||
@@ -177,15 +273,16 @@ contract RedistributedDemurrageToken {
|
||||
uint256 currentRedistribution;
|
||||
|
||||
currentRedistribution = uint256(redistributions[redistributions.length-1]);
|
||||
currentRedistribution &= 0xffffffffff0000000000000000000000000000000000000000ffffffffffffff;
|
||||
currentRedistribution |= totalSupply << 56;
|
||||
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 (uint256) {
|
||||
return (block.timestamp - periodStart) / periodDuration + 1;
|
||||
function actualPeriod() public view returns (uint128) {
|
||||
return uint128((block.timestamp - periodStart) / periodDuration + 1);
|
||||
}
|
||||
|
||||
// Add an entered demurrage period to the redistribution array
|
||||
@@ -203,14 +300,15 @@ contract RedistributedDemurrageToken {
|
||||
|
||||
// Deserialize the pemurrage period for the given account is participating in
|
||||
function accountPeriod(address _account) public view returns (uint256) {
|
||||
return (uint256(account[_account]) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) >> 160;
|
||||
return (uint256(account[_account]) & 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] &= 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff;
|
||||
account[_account] |= bytes32(_period << 160);
|
||||
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.
|
||||
@@ -245,11 +343,11 @@ contract RedistributedDemurrageToken {
|
||||
|
||||
if (truncatedResult < redistributionSupply) {
|
||||
redistributionPeriod = toRedistributionPeriod(_redistribution); // since we reuse period here, can possibly be optimized by passing period instead
|
||||
redistributions[redistributionPeriod-1] &= 0x0000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff; // just to be safe, zero out all participant count data, in this case there will be only one
|
||||
redistributions[redistributionPeriod-1] |= 0x8000000001000000000000000000000000000000000000000000000000000000;
|
||||
redistributions[redistributionPeriod-1] &= 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); //truncatedResult);
|
||||
increaseBaseBalance(sinkAddress, unit / ppmDivider);
|
||||
return unit;
|
||||
}
|
||||
|
||||
@@ -262,7 +360,8 @@ contract RedistributedDemurrageToken {
|
||||
return false;
|
||||
}
|
||||
|
||||
redistributions[_period-1] |= 0x8000000000000000000000000000000000000000000000000000000000000000;
|
||||
// TODO: is this needed?
|
||||
redistributions[_period-1] |= bytes32(maskRedistributionIsFractional);
|
||||
|
||||
periodSupply = toRedistributionSupply(redistributions[_period-1]);
|
||||
increaseBaseBalance(sinkAddress, periodSupply - _remainder);
|
||||
@@ -270,52 +369,69 @@ contract RedistributedDemurrageToken {
|
||||
}
|
||||
|
||||
|
||||
function toDemurrageAmount(uint256 _demurrage) public pure returns (uint256) {
|
||||
return _demurrage & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff;
|
||||
}
|
||||
|
||||
function toDemurragePeriod(uint256 _demurrage) public pure returns (uint256) {
|
||||
return (_demurrage & 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000) >> 128;
|
||||
}
|
||||
|
||||
// Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
|
||||
function applyDemurrage() public returns (bool) {
|
||||
uint256 epochPeriodCount;
|
||||
uint256 periodCount;
|
||||
uint128 epochPeriodCount;
|
||||
uint128 periodCount;
|
||||
uint256 lastDemurrageAmount;
|
||||
uint256 newDemurrageAmount;
|
||||
|
||||
epochPeriodCount = actualPeriod();
|
||||
//epochPeriodCount = (block.timestamp - periodStart) / periodDuration; // toDemurrageTime(demurrageModifier);
|
||||
periodCount = epochPeriodCount - toDemurragePeriod(demurrageModifier);
|
||||
periodCount = epochPeriodCount - demurragePeriod;
|
||||
if (periodCount == 0) {
|
||||
return false;
|
||||
}
|
||||
lastDemurrageAmount = toDemurrageAmount(demurrageModifier);
|
||||
newDemurrageAmount = toTaxPeriodAmount(lastDemurrageAmount, periodCount);
|
||||
demurrageModifier = 0;
|
||||
demurrageModifier |= (newDemurrageAmount & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff);
|
||||
demurrageModifier |= (epochPeriodCount << 128);
|
||||
emit Decayed(epochPeriodCount, periodCount, lastDemurrageAmount, newDemurrageAmount);
|
||||
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 applyTax() public returns (uint256) {
|
||||
function changePeriod() public returns (uint256) {
|
||||
function changePeriod() public returns (bool) {
|
||||
bytes32 currentRedistribution;
|
||||
bytes32 nextRedistribution;
|
||||
uint256 currentPeriod;
|
||||
uint256 currentParticipants;
|
||||
uint256 currentRemainder;
|
||||
uint256 currentDemurrageAmount;
|
||||
uint256 nextRedistributionDemurrage;
|
||||
uint256 demurrageCounts;
|
||||
uint256 periodTimestamp;
|
||||
uint256 nextPeriod;
|
||||
|
||||
currentRedistribution = checkPeriod();
|
||||
if (currentRedistribution == bytes32(0x00)) {
|
||||
return demurrageModifier;
|
||||
return false;
|
||||
}
|
||||
//demurrageModifier -= (demurrageModifier * taxLevel) / 1000000;
|
||||
|
||||
currentPeriod = toRedistributionPeriod(currentRedistribution);
|
||||
nextRedistribution = toRedistribution(0, totalSupply, currentPeriod + 1);
|
||||
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);
|
||||
@@ -325,15 +441,30 @@ contract RedistributedDemurrageToken {
|
||||
currentRemainder = remainder(currentParticipants, totalSupply); // we can use totalSupply directly because it will always be the same as the recorded supply on the current redistribution
|
||||
applyRemainderOnPeriod(currentRemainder, currentPeriod);
|
||||
}
|
||||
return demurrageModifier;
|
||||
emit Period(nextPeriod);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate a value reduced by demurrage by the given period
|
||||
function toTaxPeriodAmount(uint256 _value, uint256 _period) public view returns (uint256) {
|
||||
// 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;
|
||||
|
||||
// TODO: if can't get to work, reverse the iteration from current period.
|
||||
valueFactor = 1000000;
|
||||
truncatedTaxLevel = taxLevel / ppmDivider;
|
||||
|
||||
@@ -352,6 +483,7 @@ contract RedistributedDemurrageToken {
|
||||
uint256 baseValue;
|
||||
uint256 value;
|
||||
uint256 period;
|
||||
uint256 demurrage;
|
||||
|
||||
period = accountPeriod(_account);
|
||||
if (period == 0 || period >= actualPeriod()) {
|
||||
@@ -364,10 +496,12 @@ contract RedistributedDemurrageToken {
|
||||
}
|
||||
|
||||
supply = toRedistributionSupply(periodRedistribution);
|
||||
demurrage = toRedistributionDemurrageModifier(periodRedistribution);
|
||||
baseValue = ((supply / participants) * (taxLevel / 1000000)) / ppmDivider;
|
||||
value = toTaxPeriodAmount(baseValue, period - 1);
|
||||
value = (baseValue * demurrage) / 1000000;
|
||||
|
||||
account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff);
|
||||
// zero out period for the account
|
||||
account[_account] &= bytes32(~maskAccountPeriod);
|
||||
increaseBaseBalance(_account, value);
|
||||
|
||||
emit Redistribution(_account, period, value);
|
||||
@@ -376,14 +510,14 @@ contract RedistributedDemurrageToken {
|
||||
|
||||
// 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) / toDemurrageAmount(demurrageModifier);
|
||||
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;
|
||||
|
||||
applyDemurrage();
|
||||
changePeriod();
|
||||
applyRedistributionOnAccount(msg.sender);
|
||||
|
||||
@@ -393,29 +527,26 @@ 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;
|
||||
|
||||
applyDemurrage();
|
||||
changePeriod();
|
||||
applyRedistributionOnAccount(msg.sender);
|
||||
|
||||
// TODO: Prefer to truncate the result, instead it seems to round to nearest :/
|
||||
baseValue = toBaseAmount(_value);
|
||||
result = transferBase(msg.sender, _to, baseValue);
|
||||
|
||||
emit Transfer(msg.sender, _to, _value);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
|
||||
applyDemurrage();
|
||||
changePeriod();
|
||||
applyRedistributionOnAccount(msg.sender);
|
||||
|
||||
@@ -423,6 +554,7 @@ contract RedistributedDemurrageToken {
|
||||
require(allowance[_from][msg.sender] >= baseValue);
|
||||
|
||||
result = transferBase(_from, _to, baseValue);
|
||||
emit Transfer(_from, _to, _value);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -439,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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user