127 Commits

Author SHA1 Message Date
lash
17638b0a51 Bump version 2023-02-11 04:32:22 +00:00
lash
ceed2e3a7a Set immutable vars 2023-02-10 17:02:29 +00:00
lash
cc1d4c811b Remove dead console script entry 2023-02-10 16:48:08 +00:00
lash
1b167f8dd1 Bump version 2023-02-10 16:33:36 +00:00
lash
5d79399f21 Finish removing redundant tests 2023-02-10 16:19:03 +00:00
lash
5c85a8abba Fix period test to recognize actual sink address 2023-02-10 16:17:06 +00:00
lash
e6eef48808 Add supply cap 2023-02-10 15:58:31 +00:00
lash
3353733405 Add seal logic and tests 2023-02-10 15:21:37 +00:00
lash
1f0dc0aa5f Define expiry in terms of period 2023-02-10 14:30:25 +00:00
lash
09b825808f Freeze accounts balances at expiry 2023-02-10 13:45:29 +00:00
lash
2717e29d91 Freeze accounts balances at expiry 2023-02-10 13:44:35 +00:00
lash
23de062ab9 Add expiry implementation, after expire sink gets all 2023-02-10 12:22:50 +00:00
lash
c25e018cd1 Add benchmark for gas usage 2023-02-10 10:57:38 +00:00
lash
4dbbf2c9bc Remove redistribute parameter in unittest constructor 2023-02-10 10:25:43 +00:00
lash
008a6ecfba Remove redundant apply script 2023-02-10 10:14:06 +00:00
lash
550c0d60cd Add license to solidity aux dir (abdk-math) 2023-02-10 10:08:24 +00:00
lash
dc891ce9bb Rehabilitate deployer cli script 2023-02-10 10:05:10 +00:00
lash
5317573b47 All tests pass! 2023-02-10 09:06:04 +00:00
lash
34af3b1b30 Rehabilitate redistribution unit 2023-02-10 06:21:32 +00:00
lash
555b0b1724 Rehabilitate change period 2023-02-10 05:02:24 +00:00
lash
3333d50f98 Rehabilitate test growth and amounts 2023-02-09 12:26:13 +00:00
lash
e74a9cb594 test basic passes 2023-02-09 12:08:28 +00:00
lash
f785925eb5 WIP rehabilitating test basic 2023-02-09 11:44:20 +00:00
lash
ae2c1b4124 Rewrite base unittest module, reinstate more contract methods 2023-02-09 06:49:18 +00:00
lash
ffc041c1a3 WIP Implement abdk math contract for constructor, decay 2023-02-09 05:09:57 +00:00
lash
bcc957f861 Remove commented contract code, makefile single nocap only 2023-02-08 08:51:57 +00:00
lash
84b1a5b439 Return redistributions type correctly 2023-02-08 08:44:43 +00:00
lash
5941a6abdf Merge branch 'master' into dev-0.2.0 2022-05-30 07:51:34 +00:00
lash
c5de0e3300 Reactivate test, expose sinkaddress 2022-05-28 09:28:10 +00:00
lash
cc1a84f818 Add catch-up period test 2022-05-27 13:22:25 +00:00
lash
ee871730dc Remove unneeded demurragestart item 2022-05-27 12:53:11 +00:00
lash
31faa78346 Keep cumulative sink total in state and deduct from upcoming demurrage 2022-05-27 12:51:10 +00:00
lash
18ee9c5f9b Make tests pass 2022-05-27 12:02:27 +00:00
lash
a0557b35a0 Fix cumulative distribution calculation bug in SingleNocap 2022-05-27 11:10:31 +00:00
lash
127c67e665 Add steps option to demurrage cli 2022-05-03 18:19:28 +00:00
lash
1387451e01 Bump deps 2022-04-24 18:53:09 +00:00
William Luke
226f81fc5c Merge branch 'lash/apply-cli' into 'master'
Add apply demurrage cli tool

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

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

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

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

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

See merge request grassrootseconomics/sarafu-token!9
2021-05-06 08:35:58 +00:00
Louis Holbrook
1de39b90cf Lash/clean and doc 2021-05-06 08:35:58 +00:00
Louis Holbrook
e2ecc6d382 Merge branch 'lash/clean-and-doc' into 'master'
Cleanup, docs, benchmarks

See merge request grassrootseconomics/sarafu-token!8
2021-03-01 09:53:40 +00:00
Louis Holbrook
8e777aa720 Cleanup, docs, benchmarks 2021-03-01 09:53:39 +00:00
Louis Holbrook
9de5e52c2f Merge branch 'lash/cache-demurrage-modifier' into 'master'
Cache demurrage modifier in redistribution data

See merge request grassrootseconomics/sarafu-token!7
2021-02-06 19:01:34 +00:00
Louis Holbrook
528fef6444 Cache demurrage modifier in redistribution data 2021-02-06 19:01:33 +00:00
8af12b33c0 Merge branch 'timebased' into 'master'
updated to a time-frame reference with minute resolution - rather than blocks

See merge request grassrootseconomics/sarafu-token!6
2021-02-06 15:50:59 +00:00
e32001e7e0 updated to a time-frame reference with minute resolution - rather than blocks 2021-02-06 15:40:23 +00:00
Louis Holbrook
0f816ebdc5 Merge branch 'lash/use-timestamps' into 'master'
Correct base amount on mint

See merge request grassrootseconomics/sarafu-token!5
2021-02-06 14:48:40 +00:00
Louis Holbrook
0202676d51 Correct base amount on mint 2021-02-06 14:48:40 +00:00
Louis Holbrook
cf6fd57672 Merge branch 'lash/use-timestamps' into 'master'
Use timestamps instead of blocks

See merge request grassrootseconomics/sarafu-token!4
2021-02-06 14:18:30 +00:00
Louis Holbrook
0053b90368 Use timestamps instead of blocks 2021-02-06 14:18:30 +00:00
55 changed files with 5597 additions and 552 deletions

10
.gitignore vendored Normal file
View File

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

36
.gitlab-ci.yml Normal file
View File

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

View File

@@ -1,88 +1,95 @@
# RedistributedDemurrageToken
**this documentation is obsolete, will rewrite asap**
## 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.

21
python/CHANGELOG Normal file
View File

@@ -0,0 +1,21 @@
- 0.3.0
* Smart contracts use abdk math libraries, all exponential operations are static gas cost
* Add expiry features, after which balances are frozen and no more transfers or demurrage will occur
* Add sealable features for supply, sink address, expiry and minters (when sealed cannot be changed)
* Deployer script now takes demurrage amount as ppm instead of literal growth fraction
* Retire old multi and cap contracts
* Replace contract bitfields for redistributions with structs
- 0.1.1
* Settable demurrage steps for apply demurrage cli tool
- 0.1.0
* Dependency upgrades
- 0.0.11
* Apply demurrage cli tool
- 0.0.10
* Settable sink address
- 0.0.9
* Correct redistribution amount for SingleNocap contract
- 0.0.2
* Move to chainlib-eth
- 0.0.1
* Interface for redistributed and non-redistributed, with or without cap

1
python/MANIFEST.in Normal file
View File

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

View File

@@ -0,0 +1,5 @@
from .token import (
DemurrageToken,
DemurrageTokenSettings,
DemurrageRedistribution,
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import os
data_dir = os.path.realpath(os.path.dirname(__file__))

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
# external imports
from chainlib.eth.tx import (
TxFactory,
TxFormat,
)
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
abi_decode_single,
)
from chainlib.eth.constant import ZERO_ADDRESS
class ExpiryContract(TxFactory):
def set_expire_period(self, contract_address, sender_address, expire_timestamp, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('setExpirePeriod')
enc.typ(ABIContractType.UINT256)
enc.uint256(expire_timestamp)
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 expires(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('expires', contract_address, sender_address=sender_address)
@classmethod
def parse_expires(self, v):
return abi_decode_single(ABIContractType.UINT256, v)

View File

@@ -0,0 +1,175 @@
"""Deploy sarafu token
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# standard imports
import sys
import os
import json
import argparse
import logging
# external imports
import confini
from funga.eth.signer import EIP155Signer
from funga.eth.keystore.dict import DictKeystore
from chainlib.chain import ChainSpec
from chainlib.eth.nonce import (
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
)
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import receipt
from chainlib.eth.constant import ZERO_ADDRESS
import chainlib.eth.cli
from chainlib.eth.cli.arg import (
Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chainlib.eth.cli.log import process_log
from chainlib.eth.settings import process_settings
from chainlib.eth.address import to_checksum_address
from chainlib.settings import ChainSettings
from dexif import to_fixed
# local imports
import erc20_demurrage_token
from erc20_demurrage_token import (
DemurrageToken,
DemurrageTokenSettings,
)
logg = logging.getLogger()
script_dir = os.path.dirname(__file__)
data_dir = os.path.join(script_dir, '..', 'data')
config_dir = os.path.join(data_dir, 'config')
def process_config_local(config, arg, args, flags):
config.add(args.token_name, 'TOKEN_NAME')
config.add(args.token_symbol, 'TOKEN_SYMBOL')
config.add(args.token_decimals, 'TOKEN_DECIMALS')
sink_address = to_checksum_address(args.sink_address)
config.add(sink_address, 'TOKEN_SINK_ADDRESS')
config.add(args.redistribution_period, 'TOKEN_REDISTRIBUTION_PERIOD')
v = args.demurrage_level / 1000000
if v >= 1.0:
raise ValueError('demurrage level must be less than 100%')
demurrage_level = to_fixed(v)
config.add(demurrage_level, 'TOKEN_DEMURRAGE_LEVEL')
return config
arg_flags = ArgFlag()
arg = Arg(arg_flags)
flags = arg_flags.STD_WRITE | arg_flags.WALLET
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser = process_args(argparser, arg, flags)
argparser.add_argument('--name', dest='token_name', type=str, help='Token name')
argparser.add_argument('--symbol', dest='token_symbol', required=True, type=str, help='Token symbol')
argparser.add_argument('--decimals', dest='token_decimals', type=int, help='Token decimals')
argparser.add_argument('--sink-address', dest='sink_address', type=str, help='demurrage level,ppm per minute')
argparser.add_argument('--redistribution-period', type=int, help='redistribution period, minutes (0 = deactivate)') # default 10080 = week
argparser.add_argument('--demurrage-level', dest='demurrage_level', type=int, help='demurrage level, ppm per period')
args = argparser.parse_args()
logg = process_log(args, logg)
config = Config()
config = process_config(config, arg, args, flags)
config = process_config_local(config, arg, args, flags)
logg.debug('config loaded:\n{}'.format(config))
settings = ChainSettings()
settings = process_settings(settings, config)
logg.debug('settings loaded:\n{}'.format(settings))
#extra_args = {
# 'redistribution_period': 'TOKEN_REDISTRIBUTION_PERIOD',
# 'demurrage_level': 'TOKEN_DEMURRAGE_LEVEL',
# 'supply_limit': 'TOKEN_SUPPLY_LIMIT',
# 'token_name': 'TOKEN_NAME',
# 'token_symbol': 'TOKEN_SYMBOL',
# 'token_decimals': 'TOKEN_DECIMALS',
# 'sink_address': 'TOKEN_SINK_ADDRESS',
# 'multi': None,
# }
#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_fee_limit=DemurrageToken.gas(), base_config_dir=config_dir)
#
#if not bool(config.get('TOKEN_NAME')):
# logg.info('token name not set, using symbol {} as name'.format(config.get('TOKEN_SYMBOL')))
# config.add(config.get('TOKEN_SYMBOL'), 'TOKEN_NAME', True)
#
#if config.get('TOKEN_SUPPLY_LIMIT') == None:
# config.add(0, 'TOKEN_SUPPLY_LIMIT', True)
#
#if config.get('TOKEN_REDISTRIBUTION_PERIOD') == None:
# config.add(10800, 'TOKEN_REDISTRIBUTION_PERIOD', True)
#logg.debug('config loaded:\n{}'.format(config))
#
#wallet = chainlib.eth.cli.Wallet()
#wallet.from_config(config)
#
#rpc = chainlib.eth.cli.Rpc(wallet=wallet)
#conn = rpc.connect_by_config(config)
#
#chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
conn = settings.get('CONN')
c = DemurrageToken(
settings.get('CHAIN_SPEC'),
signer=settings.get('SIGNER'),
gas_oracle=settings.get('FEE_ORACLE'),
nonce_oracle=settings.get('NONCE_ORACLE'),
)
token_settings = DemurrageTokenSettings()
token_settings.name = config.get('TOKEN_NAME')
token_settings.symbol = config.get('TOKEN_SYMBOL')
token_settings.decimals = int(config.get('TOKEN_DECIMALS'))
token_settings.demurrage_level = int(config.get('TOKEN_DEMURRAGE_LEVEL'))
token_settings.period_minutes = int(config.get('TOKEN_REDISTRIBUTION_PERIOD'))
token_settings.sink_address = config.get('TOKEN_SINK_ADDRESS')
(tx_hash_hex, o) = c.constructor(
settings.get('SENDER_ADDRESS'),
token_settings,
)
if settings.get('RPC_SEND'):
conn.do(o)
if config.true('_WAIT'):
r = conn.wait(tx_hash_hex)
if r['status'] == 0:
sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you')
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)
else:
print(o)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,66 @@
# standard imports
import enum
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.tx import (
TxFactory,
TxFormat,
)
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
abi_decode_single,
)
from hexathon import (
add_0x,
)
class ContractState(enum.IntEnum):
MINTER_STATE = 1
SINK_STATE = 2
EXPIRY_STATE = 4
CAP_STATE = 8
CONTRACT_SEAL_STATE_MAX = 0
for v in dir(ContractState):
if len(v) > 6 and v[-6:] == '_STATE':
CONTRACT_SEAL_STATE_MAX += getattr(ContractState, v).value
class SealedContract(TxFactory):
def seal(self, contract_address, sender_address, seal, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('seal')
enc.typ(ABIContractType.UINT256)
enc.uint256(seal)
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 is_sealed(self, contract_address, v, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('isSealed')
enc.typ(ABIContractType.UINT256)
enc.uint256(v)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
@classmethod
def parse_is_sealed(self, v):
return abi_decode_single(ABIContractType.BOOLEAN, v)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,656 @@
# 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,
ABIContractDecoder,
ABIContractType,
abi_decode_single,
)
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.jsonrpc import JSONRPCRequest
from eth_erc20 import ERC20
from hexathon import (
add_0x,
strip_0x,
)
from dexif import from_fixed
# local imports
from erc20_demurrage_token.data import data_dir
from erc20_demurrage_token.seal import SealedContract
from erc20_demurrage_token.expiry import ExpiryContract
logg = logging.getLogger(__name__)
class DemurrageRedistribution:
def __init__(self, v):
d = ABIContractDecoder()
v = strip_0x(v)
d.typ(ABIContractType.UINT256)
d.typ(ABIContractType.UINT256)
d.typ(ABIContractType.BYTES32)
d.val(v[:64])
d.val(v[64:128])
d.val(v[128:192])
r = d.decode()
self.period = r[0]
self.value = r[1]
self.demurrage = from_fixed(r[2])
def __str__(self):
return 'period {} value {} demurrage {}'.format(self.period, self.value, self.demurrage)
class DemurrageTokenSettings:
def __init__(self):
self.name = None
self.symbol = None
self.decimals = None
self.demurrage_level = None
self.period_minutes = None
self.sink_address = None
def __str__(self):
return 'name {} demurrage level {} period minutes {} sink address {}'.format(
self.name,
self.demurrage_level,
self.period_minutes,
self.sink_address,
)
class DemurrageToken(ERC20, SealedContract, ExpiryContract):
__abi = {}
__bytecode = {}
def constructor(self, sender_address, settings, tx_format=TxFormat.JSONRPC):
code = DemurrageToken.bytecode()
enc = ABIContractEncoder()
enc.string(settings.name)
enc.string(settings.symbol)
enc.uint256(settings.decimals)
enc.uint256(settings.demurrage_level)
enc.uint256(settings.period_minutes)
enc.address(settings.sink_address)
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 4000000
@staticmethod
def abi(multi=True):
name = 'DemurrageTokenSingleNocap'
if DemurrageToken.__abi.get(name) == None:
f = open(os.path.join(data_dir, name + '.json'), 'r')
DemurrageToken.__abi[name] = json.load(f)
f.close()
return DemurrageToken.__abi[name]
@staticmethod
def bytecode(multi=True):
name = 'DemurrageTokenSingleNocap'
if DemurrageToken.__bytecode.get(name) == None:
f = open(os.path.join(data_dir, name + '.bin'), 'r')
DemurrageToken.__bytecode[name] = f.read()
f.close()
return DemurrageToken.__bytecode[name]
def increase_allowance(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('increaseAllowance')
enc.typ(ABIContractType.ADDRESS)
enc.typ(ABIContractType.UINT256)
enc.address(address)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def decrease_allowance(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('decreaseAllowance')
enc.typ(ABIContractType.ADDRESS)
enc.typ(ABIContractType.UINT256)
enc.address(address)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def add_minter(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('addMinter')
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 set_max_supply(self, contract_address, sender_address, cap, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('setMaxSupply')
enc.typ(ABIContractType.UINT256)
enc.uint256(cap)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def remove_minter(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('removeMinter')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def mint_to(self, contract_address, sender_address, address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('mintTo')
enc.typ(ABIContractType.ADDRESS)
enc.typ(ABIContractType.UINT256)
enc.address(address)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def burn(self, contract_address, sender_address, value, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('burn')
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def total_burned(self, contract_address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('totalBurned')
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def to_base_amount(self, contract_address, value, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toBaseAmount')
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def remainder(self, contract_address, parts, whole, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('remainder')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(parts)
enc.uint256(whole)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def redistributions(self, contract_address, idx, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('redistributions')
enc.typ(ABIContractType.UINT256)
enc.uint256(idx)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def account_period(self, contract_address, address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('accountPeriod')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def to_redistribution(self, contract_address, participants, demurrage_modifier, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistribution')
enc.typ(ABIContractType.UINT256)
enc.typ_literal('int128')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(participants)
enc.uint256(demurrage_modifier)
enc.uint256(value)
enc.uint256(period)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def to_redistribution_period(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionPeriod')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint64)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
# def to_redistribution_participants(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
# j = JSONRPCRequest(id_generator)
# o = j.template()
# o['method'] = 'eth_call'
# enc = ABIContractEncoder()
# enc.method('toRedistributionParticipants')
# v = strip_0x(redistribution)
# enc.typ_literal('(uint32,uint72,uint104)')
# #enc.typ(ABIContractType.BYTES32)
# enc.bytes32(v[:64])
# enc.bytes32(v[64:128])
# enc.bytes32(v[128:192])
# data = add_0x(enc.get())
# tx = self.template(sender_address, contract_address)
# tx = self.set_code(tx, data)
# o['params'].append(self.normalize(tx))
# o['params'].append('latest')
# o = j.finalize(o)
# return o
#
def to_redistribution_supply(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionSupply')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint64)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def to_redistribution_demurrage_modifier(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('toRedistributionDemurrageModifier')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint64)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def base_balance_of(self, contract_address, address, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('baseBalanceOf')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def set_sink_address(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('setSinkAddress')
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 apply_demurrage(self, contract_address, sender_address, limit=0, tx_format=TxFormat.JSONRPC):
if limit == 0:
return self.transact_noarg('applyDemurrage', contract_address, sender_address)
enc = ABIContractEncoder()
enc.method('applyDemurrageLimited')
enc.typ(ABIContractType.UINT256)
enc.uint256(limit)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def change_period(self, contract_address, sender_address):
return self.transact_noarg('changePeriod', contract_address, sender_address)
def apply_redistribution_on_account(self, contract_address, sender_address, address, tx_format=TxFormat.JSONRPC):
enc = ABIContractEncoder()
enc.method('applyRedistributionOnAccount')
enc.typ(ABIContractType.ADDRESS)
enc.address(address)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
def tax_level(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('taxLevel', contract_address, sender_address=sender_address)
def resolution_factor(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('resolutionFactor', contract_address, sender_address=sender_address)
def actual_period(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('actualPeriod', contract_address, sender_address=sender_address)
def period_start(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('periodStart', contract_address, sender_address=sender_address)
def period_duration(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('periodDuration', contract_address, sender_address=sender_address)
def demurrage_amount(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('demurrageAmount', contract_address, sender_address=sender_address)
def demurrage_timestamp(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('demurrageTimestamp', contract_address, sender_address=sender_address)
def max_supply(self, contract_address, sender_address=ZERO_ADDRESS):
return self.call_noarg('maxSupply', contract_address, sender_address=sender_address)
# def grow_by(self, contract_address, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
# j = JSONRPCRequest(id_generator)
# o = j.template()
# o['method'] = 'eth_call'
# enc = ABIContractEncoder()
# enc.method('growBy')
# enc.typ(ABIContractType.UINT256)
# enc.typ(ABIContractType.UINT256)
# enc.uint256(value)
# enc.uint256(period)
# data = add_0x(enc.get())
# tx = self.template(sender_address, contract_address)
# tx = self.set_code(tx, data)
# o['params'].append(self.normalize(tx))
# o['params'].append('latest')
# o = j.finalize(o)
# return o
#
def decay_by(self, contract_address, value, period, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('decayBy')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.UINT256)
enc.uint256(value)
enc.uint256(period)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def get_distribution(self, contract_address, supply, demurrage_amount, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('getDistribution')
enc.typ(ABIContractType.UINT256)
enc.typ_literal('int128')
enc.uint256(supply)
enc.uint256(demurrage_amount)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
def get_distribution_from_redistribution(self, contract_address, redistribution, sender_address=ZERO_ADDRESS, id_generator=None):
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method('getDistributionFromRedistribution')
v = strip_0x(redistribution)
enc.typ_literal('(uint32,uint72,uint64)')
enc.bytes32(v[:64])
enc.bytes32(v[64:128])
enc.bytes32(v[128:192])
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
o = j.finalize(o)
return o
@classmethod
def parse_actual_period(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_period_start(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_period_duration(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_demurrage_amount(self, v):
# return abi_decode_single(ABIContractType.UINT256, v)
return from_fixed(v)
@classmethod
def parse_remainder(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_to_base_amount(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_redistributions(self, v):
return strip_0x(v)
@classmethod
def parse_account_period(self, v):
return abi_decode_single(ABIContractType.ADDRESS, v)
@classmethod
def parse_to_redistribution_period(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_to_redistribution_item(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_supply_cap(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_grow_by(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_decay_by(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_get_distribution(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_tax_level(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_resolution_factor(self, v):
return abi_decode_single(ABIContractType.UINT256, v)
@classmethod
def parse_total_burned(self, v):
return abi_decode_single(ABIContractType.UINT256, v)

View File

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

View File

@@ -0,0 +1,150 @@
# standard imports
import logging
import os
import math
# external imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.tx import (
receipt,
)
from chainlib.eth.block import (
block_latest,
block_by_number,
)
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.constant import ZERO_ADDRESS
# local imports
from erc20_demurrage_token import (
DemurrageTokenSettings,
DemurrageToken,
)
from dexif import *
logg = logging.getLogger()
#BLOCKTIME = 5 # seconds
TAX_LEVEL = int(10000 * 2) # 2%
# calc "1-(0.98)^(1/518400)" <- 518400 = 30 days of blocks
# 0.00000003897127107225
PERIOD = 43200
class TestTokenDeploy:
"""tax level is ppm, 1000000 = 100%"""
def __init__(self, rpc, token_symbol='FOO', token_name='Foo Token', sink_address=ZERO_ADDRESS, tax_level=TAX_LEVEL, period=PERIOD):
self.tax_level = tax_level
self.period_seconds = period * 60
self.settings = DemurrageTokenSettings()
self.settings.name = token_name
self.settings.symbol = token_symbol
self.settings.decimals = 6
tax_level_input = to_fixed((1 - (tax_level / 1000000)) ** (1 / period))
self.settings.demurrage_level = tax_level_input
self.settings.period_minutes = period
self.settings.sink_address = sink_address
self.sink_address = self.settings.sink_address
logg.debug('using demurrage token settings: {}'.format(self.settings))
o = block_latest()
self.start_block = rpc.do(o)
o = block_by_number(self.start_block, include_tx=False)
r = rpc.do(o)
try:
self.start_time = int(r['timestamp'], 16)
except TypeError:
self.start_time = int(r['timestamp'])
def deploy(self, rpc, deployer_address, interface, supply_cap=0):
tx_hash = None
o = None
(tx_hash, o) = interface.constructor(deployer_address, self.settings)
r = rpc.do(o)
o = receipt(tx_hash)
r = rpc.do(o)
assert r['status'] == 1
self.start_block = r['block_number']
self.address = r['contract_address']
o = block_by_number(r['block_number'])
r = rpc.do(o)
self.start_time = r['timestamp']
return self.address
class TestDemurrage(EthTesterCase):
def setUp(self):
super(TestDemurrage, self).setUp()
period = PERIOD
try:
period = getattr(self, 'period')
except AttributeError as e:
pass
self.deployer = TestTokenDeploy(self.rpc, period=period, sink_address=self.accounts[9])
self.default_supply = 0
self.default_supply_cap = 0
self.start_block = None
self.address = None
self.start_time = None
def deploy(self, interface):
self.address = self.deployer.deploy(self.rpc, self.accounts[0], interface, supply_cap=self.default_supply_cap)
self.start_block = self.deployer.start_block
self.start_time = self.deployer.start_time
self.tax_level = self.deployer.tax_level
self.period_seconds = self.deployer.period_seconds
self.sink_address = self.deployer.sink_address
logg.debug('contract address {} start block {} start time {}'.format(self.address, self.start_block, self.start_time))
def assert_within(self, v, target, tolerance_ppm):
lower_target = target - (target * (tolerance_ppm / 1000000))
higher_target = target + (target * (tolerance_ppm / 1000000))
#self.assertGreaterEqual(v, lower_target)
#self.assertLessEqual(v, higher_target)
if v >= lower_target and v <= higher_target:
logg.debug('asserted within {} <= {} <= {}'.format(lower_target, v, higher_target))
return
raise AssertionError('{} not within lower {} and higher {}'.format(v, lower_target, higher_target))
def assert_within_lower(self, v, target, tolerance_ppm):
lower_target = target - (target * (tolerance_ppm / 1000000))
self.assertGreaterEqual(v, lower_target)
self.assertLessEqual(v, target)
logg.debug('asserted within lower {} <= {} <= {}'.format(lower_target, v, target))
def assert_equal_decimals(self, v, target, precision):
target = int(target * (10 ** precision))
target = target / (10 ** precision)
self.assertEqual(v, target)
def tearDown(self):
pass
class TestDemurrageDefault(TestDemurrage):
def setUp(self):
super(TestDemurrageDefault, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.deploy(c)
self.default_supply = 10**12
self.default_supply_cap = self.default_supply

View File

@@ -0,0 +1,146 @@
# standard imports
import logging
import os
import math
# external imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.tx import (
receipt,
)
from chainlib.eth.block import (
block_latest,
block_by_number,
)
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.constant import ZERO_ADDRESS
# local imports
from erc20_demurrage_token import (
DemurrageTokenSettings,
DemurrageToken,
)
from dexif import *
logg = logging.getLogger()
TAX_LEVEL = int(10000 * 2) # 2%
PERIOD = 43200 # 30 days in minutes
class TestTokenDeploy:
"""tax level is ppm, 1000000 = 100%"""
def __init__(self, rpc, token_symbol='FOO', token_name='Foo Token', sink_address=ZERO_ADDRESS, supply=10**12, tax_level=TAX_LEVEL, period=PERIOD):
self.tax_level = tax_level
self.period_seconds = period * 60
self.settings = DemurrageTokenSettings()
self.settings.name = token_name
self.settings.symbol = token_symbol
self.settings.decimals = 6
tax_level_input = to_fixed((1 - (tax_level / 1000000)) ** (1 / period))
self.settings.demurrage_level = tax_level_input
self.settings.period_minutes = period
self.settings.sink_address = sink_address
self.sink_address = self.settings.sink_address
logg.debug('using demurrage token settings: {}'.format(self.settings))
o = block_latest()
self.start_block = rpc.do(o)
o = block_by_number(self.start_block, include_tx=False)
r = rpc.do(o)
try:
self.start_time = int(r['timestamp'], 16)
except TypeError:
self.start_time = int(r['timestamp'])
self.default_supply = supply
self.default_supply_cap = 0
def deploy(self, rpc, deployer_address, interface, supply_cap=0):
tx_hash = None
o = None
(tx_hash, o) = interface.constructor(deployer_address, self.settings, redistribute=False, cap=0)
r = rpc.do(o)
o = receipt(tx_hash)
r = rpc.do(o)
assert r['status'] == 1
self.start_block = r['block_number']
self.address = r['contract_address']
o = block_by_number(r['block_number'])
r = rpc.do(o)
self.start_time = r['timestamp']
return self.address
class TestDemurrage(EthTesterCase):
def setUp(self):
super(TestDemurrage, self).setUp()
period = PERIOD
try:
period = getattr(self, 'period')
except AttributeError as e:
pass
self.deployer = TestTokenDeploy(self.rpc, period=period)
self.default_supply = self.deployer.default_supply
self.default_supply_cap = self.deployer.default_supply_cap
self.start_block = None
self.address = None
self.start_time = None
def deploy(self, interface):
self.address = self.deployer.deploy(self.rpc, self.accounts[0], interface, supply_cap=self.default_supply_cap)
self.start_block = self.deployer.start_block
self.start_time = self.deployer.start_time
self.tax_level = self.deployer.tax_level
self.period_seconds = self.deployer.period_seconds
self.sink_address = self.deployer.sink_address
logg.debug('contract address {} start block {} start time {}'.format(self.address, self.start_block, self.start_time))
def assert_within(self, v, target, tolerance_ppm):
lower_target = target - (target * (tolerance_ppm / 1000000))
higher_target = target + (target * (tolerance_ppm / 1000000))
#self.assertGreaterEqual(v, lower_target)
#self.assertLessEqual(v, higher_target)
if v >= lower_target and v <= higher_target:
logg.debug('asserted within {} <= {} <= {}'.format(lower_target, v, higher_target))
return
raise AssertionError('{} not within lower {} and higher {}'.format(v, lower_target, higher_target))
def assert_within_lower(self, v, target, tolerance_ppm):
lower_target = target - (target * (tolerance_ppm / 1000000))
self.assertGreaterEqual(v, lower_target)
self.assertLessEqual(v, target)
logg.debug('asserted within lower {} <= {} <= {}'.format(lower_target, v, target))
def assert_equal_decimals(self, v, target, precision):
target = int(target * (10 ** precision))
target = target / (10 ** precision)
self.assertEqual(v, target)
def tearDown(self):
pass
class TestDemurrageDefault(TestDemurrage):
def setUp(self):
super(TestDemurrageDefault, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
self.deploy(c)

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

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

View File

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

4
python/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
chainlib-eth~=0.4.11
eth-erc20~=0.5.0
funga-eth~=0.6.0
dexif~=0.0.1

14
python/run_tests.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -a
set -e
set -x
default_pythonpath=$PYTHONPATH:.
export PYTHONPATH=${default_pythonpath:-.}
>&2 echo using pythonpath $PYTHONPATH
for f in `ls tests/test_*.py`; do
python $f
done
set +x
set +e
set +a

43
python/setup.cfg Normal file
View File

@@ -0,0 +1,43 @@
[metadata]
name = erc20-demurrage-token
version = 0.3.1
description = ERC20 token with redistributed continual demurrage
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/ccicnet/erc20-demurrage-token
keywords =
ethereum
blockchain
cryptocurrency
erc20
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.7
packages =
erc20_demurrage_token
erc20_demurrage_token.runnable
erc20_demurrage_token.data
erc20_demurrage_token.sim
erc20_demurrage_token.unittest
[options.package_data]
* =
data/DemurrageToken*.bin
data/DemurrageToken*.json
[options.entry_points]
console_scripts =
erc20-demurrage-token-deploy = erc20_demurrage_token.runnable.deploy:main

31
python/setup.py Normal file
View 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,
)

View File

@@ -0,0 +1,2 @@
eth_tester==0.5.0b3
py-evm==0.3.0a20

228
python/tests/bench.old Normal file
View 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()

167
python/tests/bench_gas.py Normal file
View File

@@ -0,0 +1,167 @@
# standard imports
import os
import unittest
import json
import logging
import datetime
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class BenchBasic(TestDemurrageDefault):
def setUp(self):
super(BenchBasic, self).setUp()
self.bench = {
'mint': None,
'transfer_light': None,
'transfer_heavy': None,
'approve': None,
'transfer_from': None,
'period_light': None,
'period_heavy': None,
'period_catchup': None,
'demurrage': None,
}
def test_bench_min(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.bench['mint'] = r['gas_used']
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], self.accounts[2], 512)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.bench['transfer_light'] = r['gas_used']
(tx_hash, o) = c.approve(self.address, self.accounts[1], self.accounts[0], 512)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.bench['approve'] = r['gas_used']
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer_from(self.address, self.accounts[0], self.accounts[1], self.accounts[3], 256)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.bench['transfer_from'] = r['gas_used']
z = 0
for i in range(100):
self.backend.time_travel(self.start_time + int(self.period_seconds / 2) + (10 * (i * (i + 1))))
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
z += r['gas_used']
logg.info('demurrage round {} gas {}'.format(i, r['gas_used']))
z /= 100
self.bench['demurrage'] = int(z)
z = 0
for i in range(100):
self.backend.time_travel(self.start_time + (self.period_seconds * (i + 1)))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
z += r['gas_used']
logg.info('period with demurrage round {} gas {}'.format(i, r['gas_used']))
z /= 100
self.bench['period_heavy'] = int(z)
z = 0
for i in range(100):
self.backend.time_travel(self.start_time + (self.period_seconds * ((i + 101))))
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
z += r['gas_used']
logg.info('period without demurrage round {} gas {}'.format(i, r['gas_used']))
z /= 100
self.bench['period_light'] = int(z)
z = 0
self.backend.time_travel(self.start_time + (self.period_seconds * 401))
for i in range(100):
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
z += r['gas_used']
logg.info('period catchup round {} gas {}'.format(i, r['gas_used']))
z /= 100
self.bench['period_catchup'] = int(z)
self.backend.time_travel(self.start_time + (self.period_seconds * 501))
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + (self.period_seconds * 502))
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[2], self.accounts[4], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.bench['transfer_heavy'] = r['gas_used']
print(json.dumps(self.bench))
if __name__ == '__main__':
unittest.main()

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
# standard imports
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestAmounts(TestDemurrageDefault):
def test_mints(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 980)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1000)
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, 1980, 750)
self.backend.time_travel(self.start_time + self.period_seconds * 2)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
expected_balance = ((1 - self.tax_level / 1000000) ** 10) * 1000
expected_balance += ((1 - self.tax_level / 1000000) ** 20) * 1000
#self.assert_within_lower(balance, expected_balance, 500)
self.assertEqual(balance, 1940)
def test_transfers(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 2000)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0])
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 1960)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], self.accounts[2], 500)
r = self.rpc.do(o)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, 1460)
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, 500, 2000)
def test_dynamic_amount(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 2000)
r = self.rpc.do(o)
cases = [
(60, 1960),
(120, 1920),
(180, 1882),
(240, 1844),
(300, 1807),
(360, 1771),
(420, 1736),
(480, 1701),
(540, 1667),
(600, 1634),
]
for case in cases:
self.backend.time_travel(self.start_time + int(case[0] * (self.period_seconds / 60)))
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within_lower(balance, case[1], 10000)
if __name__ == '__main__':
unittest.main()

View File

@@ -3,184 +3,353 @@ import os
import unittest
import json
import logging
import datetime
# third-party imports
import web3
import eth_tester
import eth_abi
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
logging.getLogger('web3').setLevel(logging.WARNING)
logging.getLogger('eth.vm').setLevel(logging.WARNING)
testdir = os.path.dirname(__file__)
#BLOCKTIME = 5 # seconds
TAX_LEVEL = 10000 * 2 # 2%
#PERIOD = int(60/BLOCKTIME) * 60 * 24 * 30 # month
PERIOD = 10
class Test(unittest.TestCase):
contract = None
def setUp(self):
eth_params = eth_tester.backends.pyevm.main.get_default_genesis_params({
'gas_limit': 9000000,
})
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.bin'), 'r')
self.bytecode = f.read()
f.close()
f = open(os.path.join(testdir, '../../solidity/RedistributedDemurrageToken.json'), 'r')
self.abi = json.load(f)
f.close()
backend = eth_tester.PyEVMBackend(eth_params)
self.eth_tester = eth_tester.EthereumTester(backend)
provider = web3.Web3.EthereumTesterProvider(self.eth_tester)
self.w3 = web3.Web3(provider)
self.sink_address = self.w3.eth.accounts[9]
c = self.w3.eth.contract(abi=self.abi, bytecode=self.bytecode)
tx_hash = c.constructor('Foo Token', 'FOO', 6, TAX_LEVEL, PERIOD, self.sink_address).transact({'from': self.w3.eth.accounts[0]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.contract = self.w3.eth.contract(abi=self.abi, address=r.contractAddress)
self.start_block = self.w3.eth.blockNumber
def tearDown(self):
pass
class TestBasic(TestDemurrageDefault):
def test_hello(self):
self.assertEqual(self.contract.functions.actualPeriod().call(), 1)
self.eth_tester.mine_blocks(PERIOD)
self.assertEqual(self.contract.functions.actualPeriod().call(), 2)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
def test_mint(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
def test_balance(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 1024)
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 976).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
def test_apply_demurrage_limited(self):
#modifier = (10 ** 28)
modifier = 1
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
self.assertEqual(modifier, demurrage_amount)
self.backend.time_travel(self.start_time + (60 * 43200))
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0], limit=20000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
self.assert_equal_decimals(0.9906, demurrage_amount, 4)
def test_apply_demurrage(self):
#modifier = (10 ** 28)
modifier = 1
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
self.assertEqual(modifier, demurrage_amount)
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(r)
b = self.rpc.do(o)
logg.debug('block {} start {}'.format(b['timestamp'], self.start_time))
self.backend.time_travel(self.start_time + (60 * 43200))
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
self.assert_equal_decimals(0.98, demurrage_amount, 2)
self.backend.time_travel(self.start_time + (60 * 43200 * 2))
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.demurrage_amount(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_amount = c.parse_demurrage_amount(r)
modifier_base = 1000000 - self.tax_level
modifier = int(modifier_base * (10 ** 22)) # 38 decimal places minus 6 (1000000)
self.assert_equal_decimals(0.9604, demurrage_amount, 4)
def test_mint_balance(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 1024)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 976)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 2000)
self.backend.time_travel(self.start_time + (60 * 43200))
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, int(2000 * 0.98))
def test_minter_control(self):
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
(tx_hash, o) = c.add_minter(self.address, self.accounts[1], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.add_minter(self.address, self.accounts[1], self.accounts[2])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
(tx_hash, o) = c.remove_minter(self.address, self.accounts[1], self.accounts[1])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[1], self.accounts[2], 1024)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
def test_base_amount(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
self.rpc.do(o)
self.backend.time_travel(self.start_time + (60 * 43200))
(tx_hash, o) = c.apply_demurrage(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
o = c.to_base_amount(self.address, 1000, sender_address=self.accounts[0])
r = self.rpc.do(o)
amount = c.parse_to_base_amount(r)
self.assertEqual(amount, 1020)
def test_transfer(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
self.rpc.do(o)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 524)
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[1], self.accounts[2], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
balance_bob = self.contract.functions.balanceOf(self.w3.eth.accounts[2]).call()
self.assertEqual(balance_bob, 500)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 524)
tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 500)
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[2], self.accounts[1], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
def test_approve(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 600)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 0)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 600)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.increase_allowance(self.address, self.accounts[0], self.accounts[1], 200)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.decrease_allowance(self.address, self.accounts[0], self.accounts[1], 800)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.approve(self.address, self.accounts[0], self.accounts[1], 42)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
def test_transfer_from(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1024).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
self.rpc.do(o)
tx_hash = self.contract.functions.approve(self.w3.eth.accounts[2], 500).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.approve(self.address, self.accounts[1], self.accounts[2], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 1024)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 1024)
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer_from(self.address, self.accounts[2], self.accounts[1], self.accounts[3], 500)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 524)
tx_hash = self.contract.functions.transferFrom(self.w3.eth.accounts[1], self.w3.eth.accounts[3], 500).transact({'from': self.w3.eth.accounts[2]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
logg.debug('tx {}'.format(r))
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 500)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 524)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[3]).call()
self.assertEqual(balance_alice, 500)
def test_apply_tax(self):
self.eth_tester.mine_blocks(PERIOD)
tx_hash = self.contract.functions.applyTax().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(self.contract.functions.redistributionCount().call(), 2)
self.assertEqual(self.contract.functions.demurrageModifier().call(), 980000)
self.eth_tester.mine_blocks(PERIOD)
tx_hash = self.contract.functions.applyTax().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(self.contract.functions.redistributionCount().call(), 3)
self.assertEqual(self.contract.functions.demurrageModifier().call(), 960400)
def test_tax_balance(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
self.eth_tester.mine_blocks(PERIOD)
tx_hash = self.contract.functions.applyTax().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
balance = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance, 980)
def test_taxed_transfer(self):
tx_hash = self.contract.functions.mintTo(self.w3.eth.accounts[1], 1000000).transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
self.eth_tester.mine_blocks(PERIOD)
tx_hash = self.contract.functions.applyTax().transact()
r = self.w3.eth.getTransactionReceipt(tx_hash)
self.assertEqual(r.status, 1)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
self.assertEqual(balance_alice, 980000)
tx_hash = self.contract.functions.transfer(self.w3.eth.accounts[2], 500000).transact({'from': self.w3.eth.accounts[1]})
r = self.w3.eth.getTransactionReceipt(tx_hash)
logg.debug('r {}'.format(r))
self.assertEqual(r.status, 1)
balance_alice = self.contract.functions.balanceOf(self.w3.eth.accounts[1]).call()
balance_alice_trunc = int(balance_alice/1000)*1000
self.assertEqual(balance_alice_trunc, 480000)
balance_bob = self.contract.functions.balanceOf(self.w3.eth.accounts[2]).call()
balance_bob_trunc = int(balance_bob/1000)*1000
self.assertEqual(balance_bob_trunc, 500000)
(tx_hash, o) = c.transfer_from(self.address, self.accounts[2], self.accounts[1], self.accounts[3], 1)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
if __name__ == '__main__':

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

@@ -0,0 +1,66 @@
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import block_latest
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestCap(TestDemurrageDefault):
def test_cap(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
logg.debug('r {}'.format(r))
(tx_hash, o) = c.set_max_supply(self.address, self.accounts[0], self.default_supply_cap)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.max_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
cap = c.parse_supply_cap(r)
self.assertEqual(cap, self.default_supply_cap)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], self.default_supply_cap)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[2], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,98 @@
# standard imports
import os
import unittest
import json
import logging
import datetime
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestExpire(TestDemurrageDefault):
def test_expires(self):
mint_amount = self.default_supply
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
for i in range(3):
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i+1], mint_amount)
r = self.rpc.do(o)
(tx_hash, o) = c.set_expire_period(self.address, self.accounts[0], 2)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.expires(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
expiry_time = c.parse_expires(r)
self.backend.time_travel(expiry_time + 60)
o = block_latest()
r = self.rpc.do(o)
o = block_by_number(r)
r = self.rpc.do(o)
self.assertGreaterEqual(r['timestamp'], expiry_time)
nonce_oracle = RPCNonceOracle(self.sink_address, self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.sink_address, self.accounts[2], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.transfer(self.address, self.sink_address, self.accounts[2], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assert_within(balance, 0.9604 * mint_amount, 1)
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
supply = c.parse_balance(r)
(tx_hash, o) = c.change_period(self.address, self.sink_address)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
o = c.decay_by(self.address, supply, int((expiry_time - self.start_time) / 60), sender_address=self.sink_address)
r = self.rpc.do(o)
target_balance = c.parse_balance(r)
self.assert_within_lower(balance, supply - target_balance, 0.0001)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,66 @@
# standard imports
import os
import unittest
import json
import logging
import datetime
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestGrowth(TestDemurrageDefault):
def test_decay_by(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
v = 1000000000
o = c.decay_by(self.address, v, 20000, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_decay_by(r)
self.assertEqual(int(g), 990690498)
o = c.decay_by(self.address, v, 43200, sender_address=self.accounts[0])
r = self.rpc.do(o)
g = c.parse_decay_by(r)
self.assertEqual(int(g), 980000000)
def test_decay_steps(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
v = 1000000000
o = c.decay_by(self.address, v, 43200, sender_address=self.accounts[0])
r = self.rpc.do(o)
gr = c.parse_decay_by(r)
v = 1000000000
for i in range(100):
o = c.decay_by(self.address, v, 432, sender_address=self.accounts[0])
r = self.rpc.do(o)
v = c.parse_decay_by(r)
self.assert_within_lower(int(v), int(gr), 0.1)
if __name__ == '__main__':
unittest.main()

237
python/tests/test_period.py Normal file
View File

@@ -0,0 +1,237 @@
# standard imports
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import (
receipt,
TxFactory,
TxFormat,
)
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
)
from hexathon import same as hex_same
from hexathon import strip_0x
from dexif import from_fixed
# local imports
from erc20_demurrage_token import DemurrageToken
from erc20_demurrage_token import DemurrageRedistribution
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestPeriod(TestDemurrageDefault):
def test_period_and_amount(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
for i in range(100):
self.backend.time_travel(self.start_time + int((self.period_seconds / 100) * (i + 1)))
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[0], 0)
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
for lg in r['logs']:
if hex_same(lg['topics'][0], '1c9c74563c32efd114cb36fb5e432d9386c8254d08456614804a33a3088ab736'):
self.assert_equal_decimals(0.98, from_fixed(strip_0x(lg['data'])), 2)
def test_period_demurrage(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + self.period_seconds + int(self.period_seconds / 2))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
for lg in r['logs']:
if hex_same(lg['topics'][0], '1c9c74563c32efd114cb36fb5e432d9386c8254d08456614804a33a3088ab736'):
self.assert_equal_decimals(0.9701, from_fixed(strip_0x(lg['data'])), 4)
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution_data = c.parse_redistributions(r)
redistribution = DemurrageRedistribution(redistribution_data)
logg.debug('fixxx {}'.format(redistribution.demurrage))
def test_period(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 1024)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + self.period_seconds)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution = c.parse_redistributions(r)
o = c.to_redistribution_period(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_to_redistribution_period(r)
self.assertEqual(2, period)
o = c.actual_period(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = c.parse_actual_period(r)
self.assertEqual(2, period)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = from_fixed(r)
redistro = DemurrageRedistribution(redistribution)
logg.debug('redistro {} {}'.format(redistro, period))
self.backend.time_travel(self.start_time + self.period_seconds * 2)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.redistributions(self.address, 2, sender_address=self.accounts[0])
r = self.rpc.do(o)
redistribution = c.parse_redistributions(r)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
period = from_fixed(r)
# allow test code float rounding error to billionth
modifier = (1 - (self.tax_level / 1000000)) ** ((self.period_seconds * 2) / 60)
modifier *= 10 ** 9
modifier = int(modifier) * (10 ** (28 - 9))
period /= (10 ** (28 - 9))
period = int(period) * (10 ** (28 - 9))
self.assertEqual(modifier, period)
def test_change_sink(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
o = c.balance_of(self.address, ZERO_ADDRESS, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 0)
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], 102400000000)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertGreater(balance, 0)
old_sink_balance = balance
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertEqual(balance, 0)
nonce_oracle = RPCNonceOracle(self.accounts[5], self.rpc)
c = TxFactory(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
enc = ABIContractEncoder()
enc.method('setSinkAddress')
enc.typ(ABIContractType.ADDRESS)
enc.address(self.accounts[3])
data = enc.get()
o = c.template(self.accounts[5], self.address, use_nonce=True)
o = c.set_code(o, data)
(tx_hash, o) = c.finalize(o, TxFormat.JSONRPC)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = TxFactory(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
enc = ABIContractEncoder()
enc.method('setSinkAddress')
enc.typ(ABIContractType.ADDRESS)
enc.address(self.accounts[3])
data = enc.get()
o = c.template(self.accounts[0], self.address, use_nonce=True)
o = c.set_code(o, data)
(tx_hash, o) = c.finalize(o, TxFormat.JSONRPC)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.backend.time_travel(self.start_time + (self.period_seconds * 2) + 1)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertLess(balance, old_sink_balance)
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance_of(r)
self.assertGreater(balance, 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,205 @@
# standard imports
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
same as hex_same,
)
from dexif import from_fixed
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.INFO)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistribution(TestDemurrageDefault):
def test_redistribution_periods(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
supply = self.default_supply
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
self.rpc.do(o)
for i in range(1, 100):
logg.info('execute time travel to period {}'.format(i))
self.backend.time_travel(self.start_time + (self.period_seconds * i))
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
redistribution_value = 0
for lg in r['logs']:
if hex_same(lg['topics'][0], '0x9a2a887706623ad3ff7fc85652deeceabe9fe1e00466c597972079ee91ea40d3'):
redistribution_value = int(strip_0x(lg['data']), 16)
o = c.redistributions(self.address, i, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage = from_fixed(r)
o = c.redistributions(self.address, i-1, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_previous = from_fixed(r)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
logg.info('testing redistro {} sink {} mint {} adds up to {} supply {} with demurrage between {} and {}'.format(redistribution_value, balance_sink, balance_minter, balance_sink + balance_minter, supply, demurrage_previous, demurrage))
self.assert_within(balance_minter + balance_sink, supply, 10)
def test_redistribution_catchup_periods(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
supply = self.default_supply
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
self.rpc.do(o)
self.backend.time_travel(self.start_time + (self.period_seconds * 100))
balance_minter = None
balance_sink = None
real_supply = None
for i in range(1, 101):
(tx_hash, o) = c.change_period(self.address, self.accounts[0])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
real_supply = balance_sink + balance_minter
logg.info('period {} testing sink {} mint {} adds up to supply {} of original {} (delta {})'.format(i, balance_sink, balance_minter, real_supply, supply, supply - real_supply))
i = 100
o = c.redistributions(self.address, i, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage = c.parse_to_redistribution_item(r)
o = c.redistributions(self.address, i-1, sender_address=self.accounts[0])
redistribution = self.rpc.do(o)
o = c.to_redistribution_demurrage_modifier(self.address, redistribution, sender_address=self.accounts[0])
r = self.rpc.do(o)
demurrage_previous = c.parse_to_redistribution_item(r)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_sink = c.parse_balance(r)
o = c.balance_of(self.address, self.accounts[0], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance_minter = c.parse_balance(r)
logg.debug('testing sink {} mint {} adds up to supply {} with demurrage between {} and {}'.format(balance_sink, balance_minter, real_supply, demurrage_previous, demurrage))
self.assert_within_lower(balance_minter + balance_sink, supply, 0.1)
# def test_redistribution_boundaries(self):
# nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
# c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
#
# demurrage = (1 - (self.tax_level / 1000000)) * (10**28)
# supply = self.default_supply
#
# (tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[0], supply)
# self.rpc.do(o)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
# logg.debug('balance before {} supply {}'.format(balance, supply))
#
# self.backend.time_travel(self.start_time + self.period_seconds)
# (tx_hash, o) = c.change_period(self.address, self.accounts[0])
# r = self.rpc.do(o)
#
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# o = c.redistributions(self.address, 1, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# oo = c.to_redistribution_supply(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
# oo = c.to_redistribution_demurrage_modifier(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
#
# self.backend.time_travel(self.start_time + self.period_seconds * 2 + 1)
# (tx_hash, o) = c.change_period(self.address, self.accounts[0])
# r = self.rpc.do(o)
#
# o = receipt(tx_hash)
# r = self.rpc.do(o)
# self.assertEqual(r['status'], 1)
#
# o = c.redistributions(self.address, 2, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# oo = c.to_redistribution_supply(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
# oo = c.to_redistribution_demurrage_modifier(self.address, r, sender_address=self.accounts[0])
# rr = self.rpc.do(oo)
#
# o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
# r = self.rpc.do(o)
# balance = c.parse_balance(r)
if __name__ == '__main__':
unittest.main()

View File

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

195
python/tests/test_seal.py Normal file
View File

@@ -0,0 +1,195 @@
# standard imports
import os
import unittest
import json
import logging
import datetime
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import (
block_latest,
block_by_number,
)
# local imports
from erc20_demurrage_token import DemurrageToken
from erc20_demurrage_token.seal import ContractState
from erc20_demurrage_token.seal import CONTRACT_SEAL_STATE_MAX
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestSeal(TestDemurrageDefault):
def test_seal_dup(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.seal(self.address, self.accounts[0], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.seal(self.address, self.accounts[0], 1)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
def test_seal_all(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.seal(self.address, self.accounts[0], CONTRACT_SEAL_STATE_MAX)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
o = c.is_sealed(self.address, 0, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertTrue(c.parse_is_sealed(r))
def test_seal_minter(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.accounts[1])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.seal(self.address, self.accounts[0], ContractState.MINTER_STATE)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.add_minter(self.address, self.accounts[0], self.accounts[2])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
o = c.is_sealed(self.address, ContractState.MINTER_STATE, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertTrue(c.parse_is_sealed(r))
def test_seal_expiry(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.set_expire_period(self.address, self.accounts[0], 10)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_expire_period(self.address, self.accounts[0], 20)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.seal(self.address, self.accounts[0], ContractState.EXPIRY_STATE)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_expire_period(self.address, self.accounts[0], 21)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
o = c.is_sealed(self.address, ContractState.EXPIRY_STATE, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertTrue(c.parse_is_sealed(r))
def test_seal_set_sink_address(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.set_sink_address(self.address, self.accounts[0], self.accounts[3])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_sink_address(self.address, self.accounts[0], self.accounts[4])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.seal(self.address, self.accounts[0], ContractState.SINK_STATE)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_sink_address(self.address, self.accounts[0], self.accounts[5])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
o = c.is_sealed(self.address, ContractState.SINK_STATE, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertTrue(c.parse_is_sealed(r))
def test_seal_cap(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.set_max_supply(self.address, self.accounts[0], 100)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_max_supply(self.address, self.accounts[0], 200)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.seal(self.address, self.accounts[0], ContractState.CAP_STATE)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
(tx_hash, o) = c.set_max_supply(self.address, self.accounts[0], 300)
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 0)
o = c.is_sealed(self.address, ContractState.CAP_STATE, sender_address=self.accounts[0])
r = self.rpc.do(o)
self.assertTrue(c.parse_is_sealed(r))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,96 @@
import os
import unittest
import json
import logging
# external imports
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import block_latest
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
)
from dexif import to_fixed
# local imports
from erc20_demurrage_token import DemurrageToken
# test imports
from erc20_demurrage_token.unittest import TestDemurrageDefault
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
testdir = os.path.dirname(__file__)
class TestRedistributionSingle(TestDemurrageDefault):
def test_single_even_if_multiple(self):
mint_amount = 100000000
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
for i in range(3):
(tx_hash, o) = c.mint_to(self.address, self.accounts[0], self.accounts[i+1], mint_amount)
r = self.rpc.do(o)
external_address = to_checksum_address('0x' + os.urandom(20).hex())
nonce_oracle = RPCNonceOracle(self.accounts[2], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[2], external_address, int(mint_amount) * 0.1)
r = self.rpc.do(o)
nonce_oracle = RPCNonceOracle(self.accounts[3], self.rpc)
c = DemurrageToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.transfer(self.address, self.accounts[3], external_address, int(mint_amount) * 0.2)
r = self.rpc.do(o)
self.backend.time_travel(self.start_time + self.period_seconds + 1)
(tx_hash, o) = c.apply_demurrage(self.address, self.accounts[3])
self.rpc.do(o)
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
self.assertEqual(rcpt['status'], 1)
(tx_hash, o) = c.change_period(self.address, self.accounts[3])
self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
tax_modifier = 0.98
o = c.balance_of(self.address, self.accounts[1], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
self.assertEqual(balance, int(mint_amount * tax_modifier))
o = c.balance_of(self.address, self.accounts[2], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
base_amount = mint_amount - int(mint_amount * 0.1)
self.assertEqual(balance, int(base_amount * tax_modifier)) #(base_amount - (base_amount * (self.tax_level / 1000000))))
o = c.balance_of(self.address, self.accounts[3], sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
base_amount = mint_amount - int(mint_amount * 0.2)
self.assertEqual(balance, int(base_amount * tax_modifier)) #(base_amount - (base_amount * (self.tax_level / 1000000))))
o = c.total_supply(self.address, sender_address=self.accounts[0])
r = self.rpc.do(o)
new_supply = c.parse_total_supply(r)
o = c.balance_of(self.address, self.sink_address, sender_address=self.accounts[0])
r = self.rpc.do(o)
balance = c.parse_balance(r)
expected_balance = new_supply - (new_supply * tax_modifier)
self.assert_within_lower(balance, expected_balance, 1)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,703 @@
pragma solidity >= 0.8.0;
import "aux/ABDKMath64x64.sol";
// SPDX-License-Identifier: GPL-3.0-or-later
contract DemurrageTokenSingleNocap {
struct redistributionItem {
uint32 period;
uint72 value;
uint64 demurrage;
}
redistributionItem[] public redistributions; // uint51(unused) | uint64(demurrageModifier) | uint36(participants) | uint72(value) | uint32(period)
// Account balances
mapping (address => uint256) account;
// Cached demurrage amount, ppm with 38 digit resolution
//uint128 public demurrageAmount;
int128 public demurrageAmount;
// Cached demurrage timestamp; the timestamp for which demurrageAmount was last calculated
uint256 public demurrageTimestamp;
// Implements EIP172
address public owner;
address newOwner;
// Implements ERC20
string public name;
// Implements ERC20
string public symbol;
// Implements ERC20
uint256 public immutable decimals;
// Implements ERC20
//uint256 public totalSupply;
uint256 supply;
// Last executed period
uint256 public lastPeriod;
// Last sink redistribution amount
uint256 public totalSink;
// Value of burnt tokens (burnt tokens do not decay)
uint256 public burned;
// 128 bit resolution of the demurrage divisor
// (this constant x 1000000 is contained within 128 bits)
//uint256 constant nanoDivider = 100000000000000000000000000; // now nanodivider, 6 zeros less
// remaining decimal positions of nanoDivider to reach 38, equals precision in growth and decay
//uint256 constant growthResolutionFactor = 1000000000000;
// demurrage decimal width; 38 places
//uint256 public immutable resolutionFactor = nanoDivider * growthResolutionFactor;
// Timestamp of start of periods (time which contract constructor was called)
uint256 public immutable periodStart;
// Duration of a single redistribution period in seconds
uint256 public immutable periodDuration;
// Demurrage in ppm per minute
//uint256 public immutable taxLevel;
// 64x64
int128 public immutable taxLevel;
// Addresses allowed to mint new tokens
mapping (address => bool) minter;
// Storage for ERC20 approve/transferFrom methods
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
// Address to send unallocated redistribution tokens
address public sinkAddress;
// timestamp when token contract expires
uint256 public expires;
bool expired;
// supply xap
uint256 public maxSupply;
// Implements ERC20
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// Implements ERC20
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// New tokens minted
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
// New demurrage cache milestone calculated
event Decayed(uint256 indexed _period, uint256 indexed _periodCount, int128 indexed _oldAmount, int128 _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);
event Debug(int128 indexed _foo, uint256 indexed _bar);
// Emitted when tokens are burned
event Burn(address indexed _burner, uint256 _value);
// EIP173
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // EIP173
event SealStateChange(uint256 _sealState);
event Expired(uint256 _timestamp);
event Cap(uint256 indexed _oldCap, uint256 _newCap);
// property sealing
uint256 public sealState;
uint8 constant MINTER_STATE = 1;
uint8 constant SINK_STATE = 2;
uint8 constant EXPIRY_STATE = 4;
uint8 constant CAP_STATE = 8;
uint256 constant public maxSealState = 15;
constructor(string memory _name, string memory _symbol, uint8 _decimals, int128 _taxLevel, uint256 _periodMinutes, address _defaultSinkAddress) {
require(_taxLevel < (1 << 64));
redistributionItem memory initialRedistribution;
//require(ABDKMath64x64.toUInt(_taxLevel) == 0);
// ACL setup
owner = msg.sender;
minter[owner] = true;
// ERC20 setup
name = _name;
symbol = _symbol;
decimals = _decimals;
// Demurrage setup
demurrageTimestamp = block.timestamp;
periodStart = demurrageTimestamp;
periodDuration = _periodMinutes * 60;
demurrageAmount = ABDKMath64x64.fromUInt(1);
taxLevel = ABDKMath64x64.ln(_taxLevel);
initialRedistribution = toRedistribution(0, demurrageAmount, 0, 1);
redistributions.push(initialRedistribution);
// Misc settings
sinkAddress = _defaultSinkAddress;
}
function seal(uint256 _state) public returns(uint256) {
require(_state < 16, 'ERR_INVALID_STATE');
require(_state & sealState == 0, 'ERR_ALREADY_LOCKED');
sealState |= _state;
emit SealStateChange(sealState);
return uint256(sealState);
}
function isSealed(uint256 _state) public returns(bool) {
require(_state < maxSealState);
if (_state == 0) {
return sealState == maxSealState;
}
return _state & sealState == _state;
}
function setExpirePeriod(uint256 _expirePeriod) public {
uint256 r;
require(!isSealed(EXPIRY_STATE));
require(!expired);
require(msg.sender == owner);
r = periodStart + (_expirePeriod * periodDuration);
require(r > expires);
expires = r;
}
function setMaxSupply(uint256 _cap) public {
require(!isSealed(CAP_STATE));
require(msg.sender == owner);
require(_cap > totalSupply());
emit Cap(maxSupply, _cap);
maxSupply = _cap;
}
// Change sink address for redistribution
function setSinkAddress(address _sinkAddress) public {
require(!isSealed(SINK_STATE));
require(msg.sender == owner);
sinkAddress = _sinkAddress;
}
// Expire the contract if expire is set and we have gone over the threshold.
// Finalizes demurrage up to the timestamp of the expiry.
// The first approve, transfer or transferFrom call that hits the ex == 2 will get the tx mined. but without the actual effect. Otherwise we would have to wait until an external egent called applyExpiry to get the correct final balance.
function applyExpiry() public returns(uint8) {
if (expired) {
return 1;
}
if (expires == 0) {
return 0;
}
if (block.timestamp >= expires) {
applyDemurrageLimited(expires - demurrageTimestamp / 60);
expired = true;
emit Expired(block.timestamp);
changePeriod();
return 2;
}
return 0;
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(!isSealed(MINTER_STATE));
require(msg.sender == owner);
minter[_minter] = true;
return true;
}
// Given address will no longer be allowed to call the mintTo() function
function removeMinter(address _minter) public returns (bool) {
require(!isSealed(MINTER_STATE));
require(msg.sender == owner || _minter == msg.sender);
minter[_minter] = false;
return true;
}
/// Implements ERC20
function balanceOf(address _account) public view returns (uint256) {
int128 baseBalance;
int128 currentDemurragedAmount;
uint256 periodCount;
uint8 expiryState;
baseBalance = ABDKMath64x64.fromUInt(baseBalanceOf(_account));
periodCount = getMinutesDelta(demurrageTimestamp);
currentDemurragedAmount = ABDKMath64x64.mul(baseBalance, demurrageAmount);
return decayBy(ABDKMath64x64.toUInt(currentDemurragedAmount), periodCount);
}
// Balance unmodified by demurrage
function baseBalanceOf(address _account) public view returns (uint256) {
return account[_account];
}
/// Increases base balance for a single account
function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
account[_account] = oldBalance + _delta;
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
uint256 workAccount;
workAccount = uint256(account[_account]);
if (_delta == 0) {
return false;
}
oldBalance = baseBalanceOf(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
account[_account] = oldBalance - _delta;
return true;
}
// Creates new tokens out of thin air, and allocates them to the given address
// Triggers tax
function mintTo(address _beneficiary, uint256 _amount) external returns (bool) {
uint256 baseAmount;
require(applyExpiry() == 0);
require(minter[msg.sender], 'ERR_ACCESS');
changePeriod();
if (maxSupply > 0) {
require(supply + _amount <= maxSupply);
}
supply += _amount;
baseAmount = toBaseAmount(_amount);
increaseBaseBalance(_beneficiary, baseAmount);
emit Mint(msg.sender, _beneficiary, _amount);
saveRedistributionSupply();
return true;
}
// Deserializes the redistribution word
function toRedistribution(uint256 _participants, int128 _demurrageModifier, uint256 _value, uint256 _period) public pure returns(redistributionItem memory) {
redistributionItem memory redistribution;
redistribution.period = uint32(_period);
redistribution.value = uint72(_value);
redistribution.demurrage = uint64(uint128(_demurrageModifier) & 0xffffffffffffffff);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.period);
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(redistributionItem memory _redistribution) public pure returns (uint256) {
return uint256(_redistribution.value);
}
// Serializes the number of participants part of the redistribution word
function toRedistributionDemurrageModifier(redistributionItem memory _redistribution) public pure returns (int128) {
int128 r;
r = int128(int64(_redistribution.demurrage) & int128(0x0000000000000000ffffffffffffffff));
if (r == 0) {
r = ABDKMath64x64.fromUInt(1);
}
return r;
}
// Client accessor to the redistributions array length
function redistributionCount() public view returns (uint256) {
return redistributions.length;
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
redistributionItem memory currentRedistribution;
uint256 grownSupply;
grownSupply = totalSupply();
currentRedistribution = redistributions[redistributions.length-1];
currentRedistribution.value = uint72(grownSupply);
redistributions[redistributions.length-1] = currentRedistribution;
return true;
}
// Get the demurrage period of the current block number
function actualPeriod() public view returns (uint128) {
return uint128((block.timestamp - periodStart) / periodDuration + 1);
}
// Retrieve next redistribution if the period threshold has been crossed
function checkPeriod() private view returns (redistributionItem memory) {
redistributionItem memory lastRedistribution;
redistributionItem memory emptyRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[lastPeriod];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return emptyRedistribution;
}
return lastRedistribution;
}
function getDistribution(uint256 _supply, int128 _demurrageAmount) public view returns (uint256) {
int128 difference;
difference = ABDKMath64x64.mul(ABDKMath64x64.fromUInt(_supply), ABDKMath64x64.sub(ABDKMath64x64.fromUInt(1), _demurrageAmount));
return _supply - ABDKMath64x64.toUInt(difference);
}
function getDistributionFromRedistribution(redistributionItem memory _redistribution) public returns (uint256) {
uint256 redistributionSupply;
int128 redistributionDemurrage;
redistributionSupply = toRedistributionSupply(_redistribution);
redistributionDemurrage = toRedistributionDemurrageModifier(_redistribution);
return getDistribution(redistributionSupply, redistributionDemurrage);
}
// Returns the amount sent to the sink address
function applyDefaultRedistribution(redistributionItem memory _redistribution) private returns (uint256) {
uint256 unit;
uint256 baseUnit;
unit = totalSupply() - getDistributionFromRedistribution(_redistribution);
baseUnit = toBaseAmount(unit) - totalSink;
increaseBaseBalance(sinkAddress, baseUnit);
emit Redistribution(sinkAddress, _redistribution.period, unit);
lastPeriod += 1;
totalSink += baseUnit;
return unit;
}
// Recalculate the demurrage modifier for the new period
// Note that the supply for the consecutive period will be taken at the time of code execution, and thus not necessarily at the time when the redistribution period threshold was crossed.
function changePeriod() public returns (bool) {
redistributionItem memory currentRedistribution;
redistributionItem memory nextRedistribution;
redistributionItem memory lastRedistribution;
uint256 currentPeriod;
int128 lastDemurrageAmount;
int128 nextRedistributionDemurrage;
uint256 demurrageCounts;
uint256 nextPeriod;
applyDemurrage();
currentRedistribution = checkPeriod();
if (isEmptyRedistribution(currentRedistribution)) {
return false;
}
// calculate the decay from previous redistributino
lastRedistribution = redistributions[lastPeriod];
currentPeriod = toRedistributionPeriod(currentRedistribution);
nextPeriod = currentPeriod + 1;
lastDemurrageAmount = toRedistributionDemurrageModifier(lastRedistribution);
demurrageCounts = (periodDuration * currentPeriod) / 60;
// TODO refactor decayby to take int128 then DRY with it
nextRedistributionDemurrage = ABDKMath64x64.exp(ABDKMath64x64.mul(taxLevel, ABDKMath64x64.fromUInt(demurrageCounts)));
nextRedistribution = toRedistribution(0, nextRedistributionDemurrage, totalSupply(), nextPeriod);
redistributions.push(nextRedistribution);
applyDefaultRedistribution(nextRedistribution);
emit Period(nextPeriod);
return true;
}
// Calculate the time delta in whole minutes passed between given timestamp and current timestamp
function getMinutesDelta(uint256 _lastTimestamp) public view returns (uint256) {
return (block.timestamp - _lastTimestamp) / 60;
}
// Calculate and cache the demurrage value corresponding to the (period of the) time of the method call
function applyDemurrage() public returns (uint256) {
return applyDemurrageLimited(0);
}
// returns true if expired
function applyDemurrageLimited(uint256 _rounds) public returns (uint256) {
int128 v;
uint256 periodCount;
int128 periodPoint;
int128 lastDemurrageAmount;
if (expired) {
return 0;
}
periodCount = getMinutesDelta(demurrageTimestamp);
if (periodCount == 0) {
return 0;
}
lastDemurrageAmount = demurrageAmount;
// safety limit for exponential calculation to ensure that we can always
// execute this code no matter how much time passes.
if (_rounds > 0 && _rounds < periodCount) {
periodCount = _rounds;
}
periodPoint = ABDKMath64x64.fromUInt(periodCount);
v = ABDKMath64x64.mul(taxLevel, periodPoint);
v = ABDKMath64x64.exp(v);
demurrageAmount = ABDKMath64x64.mul(demurrageAmount, v);
demurrageTimestamp = demurrageTimestamp + (periodCount * 60);
emit Decayed(demurrageTimestamp, periodCount, lastDemurrageAmount, demurrageAmount);
return periodCount;
}
// 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;
}
function isEmptyRedistribution(redistributionItem memory _redistribution) public pure returns(bool) {
if (_redistribution.period > 0) {
return false;
}
if (_redistribution.value > 0) {
return false;
}
if (_redistribution.demurrage > 0) {
return false;
}
return true;
}
// Calculate a value reduced by demurrage by the given period
function decayBy(uint256 _value, uint256 _period) public view returns (uint256) {
int128 valuePoint;
int128 periodPoint;
int128 v;
valuePoint = ABDKMath64x64.fromUInt(_value);
periodPoint = ABDKMath64x64.fromUInt(_period);
v = ABDKMath64x64.mul(taxLevel, periodPoint);
v = ABDKMath64x64.exp(v);
v = ABDKMath64x64.mul(valuePoint, v);
return ABDKMath64x64.toUInt(v);
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
int128 r;
r = ABDKMath64x64.div(ABDKMath64x64.fromUInt(_value), demurrageAmount);
return ABDKMath64x64.toUInt(r);
}
// Implements ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
uint8 ex;
ex = applyExpiry();
if (ex == 2) {
return false;
} else if (ex > 0) {
revert('EXPIRED');
}
if (allowance[msg.sender][_spender] > 0) {
require(_value == 0, 'ZERO_FIRST');
}
changePeriod();
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] = baseValue;
emit Approval(msg.sender, _spender, _value);
return true;
}
// Reduce allowance by amount
function decreaseAllowance(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
baseValue = toBaseAmount(_value);
require(allowance[msg.sender][_spender] >= baseValue);
changePeriod();
allowance[msg.sender][_spender] -= baseValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
// Increase allowance by amount
function increaseAllowance(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
changePeriod();
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
// Implements ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
uint8 ex;
ex = applyExpiry();
if (ex == 2) {
return false;
} else if (ex > 0) {
revert('EXPIRED');
}
changePeriod();
baseValue = toBaseAmount(_value);
result = transferBase(msg.sender, _to, baseValue);
emit Transfer(msg.sender, _to, _value);
return result;
}
// Implements ERC20, triggers tax and/or redistribution
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
uint8 ex;
ex = applyExpiry();
if (ex == 2) {
return false;
} else if (ex > 0) {
revert('EXPIRED');
}
changePeriod();
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
allowance[_from][msg.sender] -= baseValue;
result = transferBase(_from, _to, baseValue);
emit Transfer(_from, _to, _value);
return result;
}
// ERC20 transfer backend for transfer, transferFrom
function transferBase(address _from, address _to, uint256 _value) private returns (bool) {
uint256 period;
decreaseBaseBalance(_from, _value);
increaseBaseBalance(_to, _value);
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);
}
// Explicitly and irretrievably burn tokens
// Only token minters can burn tokens
function burn(uint256 _value) public {
require(applyExpiry() == 0);
require(minter[msg.sender]);
require(_value <= account[msg.sender]);
uint256 _delta = toBaseAmount(_value);
//applyDemurrage();
decreaseBaseBalance(msg.sender, _delta);
burned += _value;
emit Burn(msg.sender, _value);
}
// Implements ERC20
function totalSupply() public view returns (uint256) {
return supply - burned;
}
// Return total number of burned tokens
function totalBurned() public view returns (uint256) {
return burned;
}
// Implements EIP165
function supportsInterface(bytes4 _sum) public pure returns (bool) {
if (_sum == 0xc6bb4b70) { // ERC20
return true;
}
if (_sum == 0x449a52f8) { // Minter
return true;
}
if (_sum == 0x01ffc9a7) { // EIP165
return true;
}
if (_sum == 0x9493f8b2) { // EIP173
return true;
}
if (_sum == 0x37a47be4) { // OwnedAccepter
return true;
}
return false;
}
}

View File

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

View File

@@ -1,360 +0,0 @@
pragma solidity > 0.6.11;
// SPDX-License-Identifier: GPL-3.0-or-later
// TODO: assign bitmask values to contants
contract RedistributedDemurrageToken {
address public owner;
uint256 public decimals;
string public name;
string public symbol;
uint256 public totalSupply;
uint256 public periodStart;
uint256 public periodDuration;
uint256 public taxLevel; // PPM
uint256 public demurrageModifier; // PPM
bytes32[] public redistributions; // uint1(isFractional) | uint1(unused) | uint38(participants) | uint160(value) | uint56(period)
mapping (address => bytes32) account; // uint20(unused) | uint56(period) | uint160(value)
mapping (address => bool) minter;
mapping (address => mapping (address => uint256 ) ) allowance; // holder -> spender -> amount (amount is subject to demurrage)
address sinkAddress; // receives redistribuion remainders
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
event Mint(address indexed _minter, address indexed _beneficiary, uint256 _value);
//event Debug(uint256 _foo);
event Taxed(uint256 indexed _period, uint256 remainder);
event Redistribution(address indexed _account, uint256 indexed _period, uint256 _value);
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint32 _taxLevel, uint256 _period, address _defaultSinkAddress) public {
owner = msg.sender;
minter[owner] = true;
periodStart = block.number;
periodDuration = _period;
taxLevel = _taxLevel;
name = _name;
symbol = _symbol;
decimals = _decimals;
demurrageModifier = 1000000;
sinkAddress = _defaultSinkAddress;
bytes32 initialRedistribution = toRedistribution(0, 0, 1);
redistributions.push(initialRedistribution);
}
// Given address will be allowed to call the mintTo() function
function addMinter(address _minter) public returns (bool) {
require(msg.sender == owner);
minter[_minter] = true;
return true;
}
/// ERC20
function balanceOf(address _account) public view returns (uint256) {
uint256 baseBalance = getBaseBalance(_account);
uint256 inverseModifier = 1000000 - demurrageModifier;
uint256 balanceModifier = (inverseModifier * baseBalance) / 1000000;
return baseBalance - balanceModifier;
}
/// Balance unmodified by demurrage
function getBaseBalance(address _account) private view returns (uint256) {
return uint256(account[_account]) & 0x00ffffffffffffffffffffffffffffffffffffffff;
}
/// Increases base balance for a single account
function increaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
oldBalance = getBaseBalance(_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);
return true;
}
/// Decreases base balance for a single account
function decreaseBaseBalance(address _account, uint256 _delta) private returns (bool) {
uint256 oldBalance;
uint256 newBalance;
oldBalance = getBaseBalance(_account);
require(oldBalance >= _delta, 'ERR_OVERSPEND'); // overspend guard
newBalance = oldBalance - _delta;
account[_account] &= bytes32(0xffffffffffffffffffffffff0000000000000000000000000000000000000000);
account[_account] |= bytes32(newBalance & 0x00ffffffffffffffffffffffffffffffffffffffff);
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) {
require(minter[msg.sender]);
// TODO: get base amount for minting
applyTax();
totalSupply += _amount;
increaseBaseBalance(_beneficiary, _amount);
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) {
bytes32 redistribution;
redistribution |= bytes32((_participants & 0x7fffffffff) << 216);
redistribution |= bytes32((_value & 0xffffffffffffffffffffffff) << 56);
redistribution |= bytes32(_period & 0xffffffffffffff);
return redistribution;
}
// Serializes the demurrage period part of the redistribution word
function toRedistributionPeriod(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x00000000000000000000000000000000000000000000000000ffffffffffffff);
}
// Serializes the supply part of the redistribution word
function toRedistributionSupply(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x0000000000ffffffffffffffffffffffffffffffffffffffff00000000000000) >> 56;
}
// Serializes the number of participants part of the redistribution word
function toRedistributionParticipants(bytes32 redistribution) public pure returns (uint256) {
return uint256(redistribution & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216;
}
// Client accessor to the redistributions array length
function redistributionCount() public view returns (uint256) {
return redistributions.length;
}
// Add number of participants for the current redistribution period by one
function incrementRedistributionParticipants() private returns (bool) {
uint256 currentRedistribution;
uint256 participants;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
participants = ((currentRedistribution & 0x7fffffffff000000000000000000000000000000000000000000000000000000) >> 216) + 1;
currentRedistribution &= 0x8000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff;
currentRedistribution |= participants << 216;
//emit Debug(participants);
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
}
// Save the current total supply amount to the current redistribution period
function saveRedistributionSupply() private returns (bool) {
uint256 currentRedistribution;
currentRedistribution = uint256(redistributions[redistributions.length-1]);
currentRedistribution &= 0xffffffffff0000000000000000000000000000000000000000ffffffffffffff;
currentRedistribution |= totalSupply << 56;
redistributions[redistributions.length-1] = bytes32(currentRedistribution);
}
// Get the demurrage period of the current block number
function actualPeriod() public view returns (uint256) {
return (block.number - periodStart) / periodDuration + 1;
}
// Add an entered demurrage period to the redistribution array
function checkPeriod() private view returns (bytes32) {
bytes32 lastRedistribution;
uint256 currentPeriod;
lastRedistribution = redistributions[redistributions.length-1];
currentPeriod = this.actualPeriod();
if (currentPeriod <= toRedistributionPeriod(lastRedistribution)) {
return bytes32(0x00);
}
return lastRedistribution;
}
// Deserialize the pemurrage period for the given account is participating in
function accountPeriod(address _account) public view returns (uint256) {
return (uint256(account[_account]) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) >> 160;
}
// 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);
incrementRedistributionParticipants();
}
// Determine whether the unit number is rounded down, rounded up or evenly divides.
// Returns 0 if evenly distributed, or the remainder as a positive number
// A _numParts value 0 will be interpreted as the value 1
function remainder(uint256 _numParts, uint256 _sumWhole) public pure returns (uint256) {
uint256 unit;
uint256 truncatedResult;
if (_numParts == 0) { // no division by zero please
return _sumWhole;
}
require(_numParts < _sumWhole); // At least you are never LESS than the sum of your parts. Think about that.
unit = _sumWhole / _numParts;
truncatedResult = unit * _numParts;
return _sumWhole - truncatedResult;
}
// sets the remainder bit for the given period and books the remainder to the sink address balance
// returns false if no change was made
function applyRemainderOnPeriod(uint256 _remainder, uint256 _period) private returns (bool) {
bytes32 redistribution;
redistribution = redistributions[_period-1];
if (_remainder == 0) {
return false;
}
redistribution |= 0x8000000000000000000000000000000000000000000000000000000000000000;
increaseBaseBalance(sinkAddress, _remainder);
return true;
}
// 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) {
bytes32 currentRedistribution;
bytes32 nextRedistribution;
uint256 currentPeriod;
uint256 currentParticipants;
uint256 currentRemainder;
currentRedistribution = checkPeriod();
if (currentRedistribution == bytes32(0x00)) {
return demurrageModifier;
}
demurrageModifier -= (demurrageModifier * taxLevel) / 1000000;
currentPeriod = toRedistributionPeriod(currentRedistribution);
nextRedistribution = toRedistribution(0, totalSupply, currentPeriod + 1);
redistributions.push(nextRedistribution);
currentParticipants = toRedistributionParticipants(currentRedistribution);
currentRemainder = remainder(currentParticipants, totalSupply); // we can use totalSupply directly because it will always be the same as the recorded supply on the current redistribution
applyRemainderOnPeriod(currentRemainder, currentPeriod);
emit Taxed(currentPeriod, currentRemainder);
return demurrageModifier;
}
// Calculate a value reduced by demurrage by the given period
function toTaxPeriodAmount(uint256 _value, uint256 _period) public view returns (uint256) {
uint256 valueFactor;
// TODO: doesn't work for solidity as floats are missing and using ints linearly increases the order of magnitude
// valueFactor = 1000000 * (((1000000-taxLevel)/1000000) ** _period);
valueFactor = 1000000;
for (uint256 i = 0; i < _period; i++) {
valueFactor = (valueFactor * taxLevel) / 1000000;
}
return (valueFactor * _value) / 1000000;
}
// If the given account is participating in a period and that period has been crossed
// THEN increase the base value of the account with its share of the value reduction of the period
function applyRedistributionOnAccount(address _account) public returns (bool) {
bytes32 periodRedistribution;
uint256 supply;
uint256 participants;
uint256 baseValue;
uint256 value;
uint256 period;
period = accountPeriod(_account);
if (period == 0 || period >= actualPeriod()) {
return false;
}
periodRedistribution = redistributions[period-1];
participants = toRedistributionParticipants(periodRedistribution);
if (participants == 0) {
return false;
}
supply = toRedistributionSupply(periodRedistribution);
// TODO: Make sure value for balance increases round down, and that we can do a single allocation to a sink account with the difference. We can use the highest bit in "participants" for that.
baseValue = supply / participants;
value = toTaxPeriodAmount(baseValue, period-1);
account[_account] &= bytes32(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff);
increaseBaseBalance(_account, value);
emit Redistribution(_account, period, value);
return true;
}
// Inflates the given amount according to the current demurrage modifier
function toBaseAmount(uint256 _value) public view returns (uint256) {
return (_value * 1000000) / demurrageModifier;
}
// ERC20, triggers tax and/or redistribution
function transfer(address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
applyTax();
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);
return result;
}
// ERC20 transfer backend for transfer, transferFrom
function transferBase(address _from, address _to, uint256 _value) private returns (bool) {
uint256 period;
if (!decreaseBaseBalance(_from, _value)) {
revert('ERR_TX_DECREASEBALANCE');
}
if (!increaseBaseBalance(_to, _value)) {
revert('ERR_TX_INCREASEBALANCE');
}
period = actualPeriod();
if (_value > 0 && accountPeriod(_from) != period) {
registerAccountPeriod(_from, period);
}
return true;
}
// ERC20, triggers tax and/or redistribution
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
uint256 baseValue;
bool result;
applyTax();
applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
require(allowance[_from][msg.sender] >= baseValue);
result = transferBase(_from, _to, baseValue);
return result;
}
// ERC20, triggers tax and/or redistribution
function approve(address _spender, uint256 _value) public returns (bool) {
uint256 baseValue;
applyTax();
applyRedistributionOnAccount(msg.sender);
baseValue = toBaseAmount(_value);
allowance[msg.sender][_spender] += baseValue;
emit Approval(msg.sender, _spender, _value);
return true;
}
}

View File

@@ -0,0 +1,752 @@
// SPDX-License-Identifier: BSD-4-Clause
/*
* ABDK Math 64.64 Smart Contract Library. Copyright © 2019 by ABDK Consulting.
* Author: Mikhail Vladimirov <mikhail.vladimirov@gmail.com>
*/
pragma solidity ^0.8.0;
/**
* Smart contract library of mathematical functions operating with signed
* 64.64-bit fixed point numbers. Signed 64.64-bit fixed point number is
* basically a simple fraction whose numerator is signed 128-bit integer and
* denominator is 2^64. As long as denominator is always the same, there is no
* need to store it, thus in Solidity signed 64.64-bit fixed point numbers are
* represented by int128 type holding only the numerator.
*/
library ABDKMath64x64 {
/*
* Minimum value signed 64.64-bit fixed point number may have.
*/
int128 private constant MIN_64x64 = -0x80000000000000000000000000000000;
/*
* Maximum value signed 64.64-bit fixed point number may have.
*/
int128 private constant MAX_64x64 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
/**
* Convert signed 256-bit integer number into signed 64.64-bit fixed point
* number. Revert on overflow.
*
* @param x signed 256-bit integer number
* @return signed 64.64-bit fixed point number
*/
function fromInt (int256 x) internal pure returns (int128) {
unchecked {
require (x >= -0x8000000000000000 && x <= 0x7FFFFFFFFFFFFFFF);
return int128 (x << 64);
}
}
/**
* Convert signed 64.64 fixed point number into signed 64-bit integer number
* rounding down.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64-bit integer number
*/
function toInt (int128 x) internal pure returns (int64) {
unchecked {
return int64 (x >> 64);
}
}
/**
* Convert unsigned 256-bit integer number into signed 64.64-bit fixed point
* number. Revert on overflow.
*
* @param x unsigned 256-bit integer number
* @return signed 64.64-bit fixed point number
*/
function fromUInt (uint256 x) internal pure returns (int128) {
unchecked {
require (x <= 0x7FFFFFFFFFFFFFFF);
return int128 (int256 (x << 64));
}
}
/**
* Convert signed 64.64 fixed point number into unsigned 64-bit integer
* number rounding down. Revert on underflow.
*
* @param x signed 64.64-bit fixed point number
* @return unsigned 64-bit integer number
*/
function toUInt (int128 x) internal pure returns (uint64) {
unchecked {
require (x >= 0);
return uint64 (uint128 (x >> 64));
}
}
/**
* Convert signed 128.128 fixed point number into signed 64.64-bit fixed point
* number rounding down. Revert on overflow.
*
* @param x signed 128.128-bin fixed point number
* @return signed 64.64-bit fixed point number
*/
function from128x128 (int256 x) internal pure returns (int128) {
unchecked {
int256 result = x >> 64;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Convert signed 64.64 fixed point number into signed 128.128 fixed point
* number.
*
* @param x signed 64.64-bit fixed point number
* @return signed 128.128 fixed point number
*/
function to128x128 (int128 x) internal pure returns (int256) {
unchecked {
return int256 (x) << 64;
}
}
/**
* Calculate x + y. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function add (int128 x, int128 y) internal pure returns (int128) {
unchecked {
int256 result = int256(x) + y;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate x - y. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function sub (int128 x, int128 y) internal pure returns (int128) {
unchecked {
int256 result = int256(x) - y;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate x * y rounding down. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function mul (int128 x, int128 y) internal pure returns (int128) {
unchecked {
int256 result = int256(x) * y >> 64;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate x * y rounding towards zero, where x is signed 64.64 fixed point
* number and y is signed 256-bit integer number. Revert on overflow.
*
* @param x signed 64.64 fixed point number
* @param y signed 256-bit integer number
* @return signed 256-bit integer number
*/
function muli (int128 x, int256 y) internal pure returns (int256) {
unchecked {
if (x == MIN_64x64) {
require (y >= -0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF &&
y <= 0x1000000000000000000000000000000000000000000000000);
return -y << 63;
} else {
bool negativeResult = false;
if (x < 0) {
x = -x;
negativeResult = true;
}
if (y < 0) {
y = -y; // We rely on overflow behavior here
negativeResult = !negativeResult;
}
uint256 absoluteResult = mulu (x, uint256 (y));
if (negativeResult) {
require (absoluteResult <=
0x8000000000000000000000000000000000000000000000000000000000000000);
return -int256 (absoluteResult); // We rely on overflow behavior here
} else {
require (absoluteResult <=
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
return int256 (absoluteResult);
}
}
}
}
/**
* Calculate x * y rounding down, where x is signed 64.64 fixed point number
* and y is unsigned 256-bit integer number. Revert on overflow.
*
* @param x signed 64.64 fixed point number
* @param y unsigned 256-bit integer number
* @return unsigned 256-bit integer number
*/
function mulu (int128 x, uint256 y) internal pure returns (uint256) {
unchecked {
if (y == 0) return 0;
require (x >= 0);
uint256 lo = (uint256 (int256 (x)) * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) >> 64;
uint256 hi = uint256 (int256 (x)) * (y >> 128);
require (hi <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
hi <<= 64;
require (hi <=
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - lo);
return hi + lo;
}
}
/**
* Calculate x / y rounding towards zero. Revert on overflow or when y is
* zero.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function div (int128 x, int128 y) internal pure returns (int128) {
unchecked {
require (y != 0);
int256 result = (int256 (x) << 64) / y;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate x / y rounding towards zero, where x and y are signed 256-bit
* integer numbers. Revert on overflow or when y is zero.
*
* @param x signed 256-bit integer number
* @param y signed 256-bit integer number
* @return signed 64.64-bit fixed point number
*/
function divi (int256 x, int256 y) internal pure returns (int128) {
unchecked {
require (y != 0);
bool negativeResult = false;
if (x < 0) {
x = -x; // We rely on overflow behavior here
negativeResult = true;
}
if (y < 0) {
y = -y; // We rely on overflow behavior here
negativeResult = !negativeResult;
}
uint128 absoluteResult = divuu (uint256 (x), uint256 (y));
if (negativeResult) {
require (absoluteResult <= 0x80000000000000000000000000000000);
return -int128 (absoluteResult); // We rely on overflow behavior here
} else {
require (absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
return int128 (absoluteResult); // We rely on overflow behavior here
}
}
}
/**
* Calculate x / y rounding towards zero, where x and y are unsigned 256-bit
* integer numbers. Revert on overflow or when y is zero.
*
* @param x unsigned 256-bit integer number
* @param y unsigned 256-bit integer number
* @return signed 64.64-bit fixed point number
*/
function divu (uint256 x, uint256 y) internal pure returns (int128) {
unchecked {
require (y != 0);
uint128 result = divuu (x, y);
require (result <= uint128 (MAX_64x64));
return int128 (result);
}
}
/**
* Calculate -x. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function neg (int128 x) internal pure returns (int128) {
unchecked {
require (x != MIN_64x64);
return -x;
}
}
/**
* Calculate |x|. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function abs (int128 x) internal pure returns (int128) {
unchecked {
require (x != MIN_64x64);
return x < 0 ? -x : x;
}
}
/**
* Calculate 1 / x rounding towards zero. Revert on overflow or when x is
* zero.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function inv (int128 x) internal pure returns (int128) {
unchecked {
require (x != 0);
int256 result = int256 (0x100000000000000000000000000000000) / x;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate arithmetics average of x and y, i.e. (x + y) / 2 rounding down.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function avg (int128 x, int128 y) internal pure returns (int128) {
unchecked {
return int128 ((int256 (x) + int256 (y)) >> 1);
}
}
/**
* Calculate geometric average of x and y, i.e. sqrt (x * y) rounding down.
* Revert on overflow or in case x * y is negative.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function gavg (int128 x, int128 y) internal pure returns (int128) {
unchecked {
int256 m = int256 (x) * int256 (y);
require (m >= 0);
require (m <
0x4000000000000000000000000000000000000000000000000000000000000000);
return int128 (sqrtu (uint256 (m)));
}
}
/**
* Calculate x^y assuming 0^0 is 1, where x is signed 64.64 fixed point number
* and y is unsigned 256-bit integer number. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @param y uint256 value
* @return signed 64.64-bit fixed point number
*/
function pow (int128 x, uint256 y) internal pure returns (int128) {
unchecked {
bool negative = x < 0 && y & 1 == 1;
uint256 absX = uint128 (x < 0 ? -x : x);
uint256 absResult;
absResult = 0x100000000000000000000000000000000;
if (absX <= 0x10000000000000000) {
absX <<= 63;
while (y != 0) {
if (y & 0x1 != 0) {
absResult = absResult * absX >> 127;
}
absX = absX * absX >> 127;
if (y & 0x2 != 0) {
absResult = absResult * absX >> 127;
}
absX = absX * absX >> 127;
if (y & 0x4 != 0) {
absResult = absResult * absX >> 127;
}
absX = absX * absX >> 127;
if (y & 0x8 != 0) {
absResult = absResult * absX >> 127;
}
absX = absX * absX >> 127;
y >>= 4;
}
absResult >>= 64;
} else {
uint256 absXShift = 63;
if (absX < 0x1000000000000000000000000) { absX <<= 32; absXShift -= 32; }
if (absX < 0x10000000000000000000000000000) { absX <<= 16; absXShift -= 16; }
if (absX < 0x1000000000000000000000000000000) { absX <<= 8; absXShift -= 8; }
if (absX < 0x10000000000000000000000000000000) { absX <<= 4; absXShift -= 4; }
if (absX < 0x40000000000000000000000000000000) { absX <<= 2; absXShift -= 2; }
if (absX < 0x80000000000000000000000000000000) { absX <<= 1; absXShift -= 1; }
uint256 resultShift = 0;
while (y != 0) {
require (absXShift < 64);
if (y & 0x1 != 0) {
absResult = absResult * absX >> 127;
resultShift += absXShift;
if (absResult > 0x100000000000000000000000000000000) {
absResult >>= 1;
resultShift += 1;
}
}
absX = absX * absX >> 127;
absXShift <<= 1;
if (absX >= 0x100000000000000000000000000000000) {
absX >>= 1;
absXShift += 1;
}
y >>= 1;
}
require (resultShift < 64);
absResult >>= 64 - resultShift;
}
int256 result = negative ? -int256 (absResult) : int256 (absResult);
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
/**
* Calculate sqrt (x) rounding down. Revert if x < 0.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function sqrt (int128 x) internal pure returns (int128) {
unchecked {
require (x >= 0);
return int128 (sqrtu (uint256 (int256 (x)) << 64));
}
}
/**
* Calculate binary logarithm of x. Revert if x <= 0.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function log_2 (int128 x) internal pure returns (int128) {
unchecked {
require (x > 0);
int256 msb = 0;
int256 xc = x;
if (xc >= 0x10000000000000000) { xc >>= 64; msb += 64; }
if (xc >= 0x100000000) { xc >>= 32; msb += 32; }
if (xc >= 0x10000) { xc >>= 16; msb += 16; }
if (xc >= 0x100) { xc >>= 8; msb += 8; }
if (xc >= 0x10) { xc >>= 4; msb += 4; }
if (xc >= 0x4) { xc >>= 2; msb += 2; }
if (xc >= 0x2) msb += 1; // No need to shift xc anymore
int256 result = msb - 64 << 64;
uint256 ux = uint256 (int256 (x)) << uint256 (127 - msb);
for (int256 bit = 0x8000000000000000; bit > 0; bit >>= 1) {
ux *= ux;
uint256 b = ux >> 255;
ux >>= 127 + b;
result += bit * int256 (b);
}
return int128 (result);
}
}
/**
* Calculate natural logarithm of x. Revert if x <= 0.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function ln (int128 x) internal pure returns (int128) {
unchecked {
require (x > 0);
return int128 (int256 (
uint256 (int256 (log_2 (x))) * 0xB17217F7D1CF79ABC9E3B39803F2F6AF >> 128));
}
}
/**
* Calculate binary exponent of x. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function exp_2 (int128 x) internal pure returns (int128) {
unchecked {
require (x < 0x400000000000000000); // Overflow
if (x < -0x400000000000000000) return 0; // Underflow
uint256 result = 0x80000000000000000000000000000000;
if (x & 0x8000000000000000 > 0)
result = result * 0x16A09E667F3BCC908B2FB1366EA957D3E >> 128;
if (x & 0x4000000000000000 > 0)
result = result * 0x1306FE0A31B7152DE8D5A46305C85EDEC >> 128;
if (x & 0x2000000000000000 > 0)
result = result * 0x1172B83C7D517ADCDF7C8C50EB14A791F >> 128;
if (x & 0x1000000000000000 > 0)
result = result * 0x10B5586CF9890F6298B92B71842A98363 >> 128;
if (x & 0x800000000000000 > 0)
result = result * 0x1059B0D31585743AE7C548EB68CA417FD >> 128;
if (x & 0x400000000000000 > 0)
result = result * 0x102C9A3E778060EE6F7CACA4F7A29BDE8 >> 128;
if (x & 0x200000000000000 > 0)
result = result * 0x10163DA9FB33356D84A66AE336DCDFA3F >> 128;
if (x & 0x100000000000000 > 0)
result = result * 0x100B1AFA5ABCBED6129AB13EC11DC9543 >> 128;
if (x & 0x80000000000000 > 0)
result = result * 0x10058C86DA1C09EA1FF19D294CF2F679B >> 128;
if (x & 0x40000000000000 > 0)
result = result * 0x1002C605E2E8CEC506D21BFC89A23A00F >> 128;
if (x & 0x20000000000000 > 0)
result = result * 0x100162F3904051FA128BCA9C55C31E5DF >> 128;
if (x & 0x10000000000000 > 0)
result = result * 0x1000B175EFFDC76BA38E31671CA939725 >> 128;
if (x & 0x8000000000000 > 0)
result = result * 0x100058BA01FB9F96D6CACD4B180917C3D >> 128;
if (x & 0x4000000000000 > 0)
result = result * 0x10002C5CC37DA9491D0985C348C68E7B3 >> 128;
if (x & 0x2000000000000 > 0)
result = result * 0x1000162E525EE054754457D5995292026 >> 128;
if (x & 0x1000000000000 > 0)
result = result * 0x10000B17255775C040618BF4A4ADE83FC >> 128;
if (x & 0x800000000000 > 0)
result = result * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB >> 128;
if (x & 0x400000000000 > 0)
result = result * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9 >> 128;
if (x & 0x200000000000 > 0)
result = result * 0x10000162E43F4F831060E02D839A9D16D >> 128;
if (x & 0x100000000000 > 0)
result = result * 0x100000B1721BCFC99D9F890EA06911763 >> 128;
if (x & 0x80000000000 > 0)
result = result * 0x10000058B90CF1E6D97F9CA14DBCC1628 >> 128;
if (x & 0x40000000000 > 0)
result = result * 0x1000002C5C863B73F016468F6BAC5CA2B >> 128;
if (x & 0x20000000000 > 0)
result = result * 0x100000162E430E5A18F6119E3C02282A5 >> 128;
if (x & 0x10000000000 > 0)
result = result * 0x1000000B1721835514B86E6D96EFD1BFE >> 128;
if (x & 0x8000000000 > 0)
result = result * 0x100000058B90C0B48C6BE5DF846C5B2EF >> 128;
if (x & 0x4000000000 > 0)
result = result * 0x10000002C5C8601CC6B9E94213C72737A >> 128;
if (x & 0x2000000000 > 0)
result = result * 0x1000000162E42FFF037DF38AA2B219F06 >> 128;
if (x & 0x1000000000 > 0)
result = result * 0x10000000B17217FBA9C739AA5819F44F9 >> 128;
if (x & 0x800000000 > 0)
result = result * 0x1000000058B90BFCDEE5ACD3C1CEDC823 >> 128;
if (x & 0x400000000 > 0)
result = result * 0x100000002C5C85FE31F35A6A30DA1BE50 >> 128;
if (x & 0x200000000 > 0)
result = result * 0x10000000162E42FF0999CE3541B9FFFCF >> 128;
if (x & 0x100000000 > 0)
result = result * 0x100000000B17217F80F4EF5AADDA45554 >> 128;
if (x & 0x80000000 > 0)
result = result * 0x10000000058B90BFBF8479BD5A81B51AD >> 128;
if (x & 0x40000000 > 0)
result = result * 0x1000000002C5C85FDF84BD62AE30A74CC >> 128;
if (x & 0x20000000 > 0)
result = result * 0x100000000162E42FEFB2FED257559BDAA >> 128;
if (x & 0x10000000 > 0)
result = result * 0x1000000000B17217F7D5A7716BBA4A9AE >> 128;
if (x & 0x8000000 > 0)
result = result * 0x100000000058B90BFBE9DDBAC5E109CCE >> 128;
if (x & 0x4000000 > 0)
result = result * 0x10000000002C5C85FDF4B15DE6F17EB0D >> 128;
if (x & 0x2000000 > 0)
result = result * 0x1000000000162E42FEFA494F1478FDE05 >> 128;
if (x & 0x1000000 > 0)
result = result * 0x10000000000B17217F7D20CF927C8E94C >> 128;
if (x & 0x800000 > 0)
result = result * 0x1000000000058B90BFBE8F71CB4E4B33D >> 128;
if (x & 0x400000 > 0)
result = result * 0x100000000002C5C85FDF477B662B26945 >> 128;
if (x & 0x200000 > 0)
result = result * 0x10000000000162E42FEFA3AE53369388C >> 128;
if (x & 0x100000 > 0)
result = result * 0x100000000000B17217F7D1D351A389D40 >> 128;
if (x & 0x80000 > 0)
result = result * 0x10000000000058B90BFBE8E8B2D3D4EDE >> 128;
if (x & 0x40000 > 0)
result = result * 0x1000000000002C5C85FDF4741BEA6E77E >> 128;
if (x & 0x20000 > 0)
result = result * 0x100000000000162E42FEFA39FE95583C2 >> 128;
if (x & 0x10000 > 0)
result = result * 0x1000000000000B17217F7D1CFB72B45E1 >> 128;
if (x & 0x8000 > 0)
result = result * 0x100000000000058B90BFBE8E7CC35C3F0 >> 128;
if (x & 0x4000 > 0)
result = result * 0x10000000000002C5C85FDF473E242EA38 >> 128;
if (x & 0x2000 > 0)
result = result * 0x1000000000000162E42FEFA39F02B772C >> 128;
if (x & 0x1000 > 0)
result = result * 0x10000000000000B17217F7D1CF7D83C1A >> 128;
if (x & 0x800 > 0)
result = result * 0x1000000000000058B90BFBE8E7BDCBE2E >> 128;
if (x & 0x400 > 0)
result = result * 0x100000000000002C5C85FDF473DEA871F >> 128;
if (x & 0x200 > 0)
result = result * 0x10000000000000162E42FEFA39EF44D91 >> 128;
if (x & 0x100 > 0)
result = result * 0x100000000000000B17217F7D1CF79E949 >> 128;
if (x & 0x80 > 0)
result = result * 0x10000000000000058B90BFBE8E7BCE544 >> 128;
if (x & 0x40 > 0)
result = result * 0x1000000000000002C5C85FDF473DE6ECA >> 128;
if (x & 0x20 > 0)
result = result * 0x100000000000000162E42FEFA39EF366F >> 128;
if (x & 0x10 > 0)
result = result * 0x1000000000000000B17217F7D1CF79AFA >> 128;
if (x & 0x8 > 0)
result = result * 0x100000000000000058B90BFBE8E7BCD6D >> 128;
if (x & 0x4 > 0)
result = result * 0x10000000000000002C5C85FDF473DE6B2 >> 128;
if (x & 0x2 > 0)
result = result * 0x1000000000000000162E42FEFA39EF358 >> 128;
if (x & 0x1 > 0)
result = result * 0x10000000000000000B17217F7D1CF79AB >> 128;
result >>= uint256 (int256 (63 - (x >> 64)));
require (result <= uint256 (int256 (MAX_64x64)));
return int128 (int256 (result));
}
}
/**
* Calculate natural exponent of x. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function exp (int128 x) internal pure returns (int128) {
unchecked {
require (x < 0x400000000000000000); // Overflow
if (x < -0x400000000000000000) return 0; // Underflow
return exp_2 (
int128 (int256 (x) * 0x171547652B82FE1777D0FFDA0D23A7D12 >> 128));
}
}
/**
* Calculate x / y rounding towards zero, where x and y are unsigned 256-bit
* integer numbers. Revert on overflow or when y is zero.
*
* @param x unsigned 256-bit integer number
* @param y unsigned 256-bit integer number
* @return unsigned 64.64-bit fixed point number
*/
function divuu (uint256 x, uint256 y) private pure returns (uint128) {
unchecked {
require (y != 0);
uint256 result;
if (x <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
result = (x << 64) / y;
else {
uint256 msb = 192;
uint256 xc = x >> 192;
if (xc >= 0x100000000) { xc >>= 32; msb += 32; }
if (xc >= 0x10000) { xc >>= 16; msb += 16; }
if (xc >= 0x100) { xc >>= 8; msb += 8; }
if (xc >= 0x10) { xc >>= 4; msb += 4; }
if (xc >= 0x4) { xc >>= 2; msb += 2; }
if (xc >= 0x2) msb += 1; // No need to shift xc anymore
result = (x << 255 - msb) / ((y - 1 >> msb - 191) + 1);
require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
uint256 hi = result * (y >> 128);
uint256 lo = result * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
uint256 xh = x >> 192;
uint256 xl = x << 64;
if (xl < lo) xh -= 1;
xl -= lo; // We rely on overflow behavior here
lo = hi << 128;
if (xl < lo) xh -= 1;
xl -= lo; // We rely on overflow behavior here
assert (xh == hi >> 128);
result += xl / y;
}
require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
return uint128 (result);
}
}
/**
* Calculate sqrt (x) rounding down, where x is unsigned 256-bit integer
* number.
*
* @param x unsigned 256-bit integer number
* @return unsigned 128-bit integer number
*/
function sqrtu (uint256 x) private pure returns (uint128) {
unchecked {
if (x == 0) return 0;
else {
uint256 xx = x;
uint256 r = 1;
if (xx >= 0x100000000000000000000000000000000) { xx >>= 128; r <<= 64; }
if (xx >= 0x10000000000000000) { xx >>= 64; r <<= 32; }
if (xx >= 0x100000000) { xx >>= 32; r <<= 16; }
if (xx >= 0x10000) { xx >>= 16; r <<= 8; }
if (xx >= 0x100) { xx >>= 8; r <<= 4; }
if (xx >= 0x10) { xx >>= 4; r <<= 2; }
if (xx >= 0x4) { r <<= 1; }
r = (r + x / r) >> 1;
r = (r + x / r) >> 1;
r = (r + x / r) >> 1;
r = (r + x / r) >> 1;
r = (r + x / r) >> 1;
r = (r + x / r) >> 1;
r = (r + x / r) >> 1; // Seven iterations should be enough
uint256 r1 = x / r;
return uint128 (r < r1 ? r : r1);
}
}
}
}

28
solidity/aux/LICENSE.md Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2019, [ABDK Consulting](https://abdk.consulting/)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must
display the following acknowledgement: This product includes software
developed by ABDK Consulting.
4. Neither the name of ABDK Consulting nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY ABDK CONSULTING ''AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL ABDK CONSULTING BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.