erc20-pool

Permissioned ERC20 swap pool for EVM
Log | Files | Refs | README

commit 5a1114ae55169924ca1c0fcf5ca425e4d22f48b5
parent af059d24a7a8d0e8a9fd7683ac7704fe838133db
Author: lash <dev@holbrook.no>
Date:   Thu,  3 Aug 2023 14:53:30 +0100

Add quoter example contract for decimals translation

Diffstat:
AREADME.md | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/erc20_pool/data/DecimalQuote.bin | 2++
Apython/erc20_pool/data/DecimalQuote.json | 1+
Apython/erc20_pool/data/DecimalQuote.metadata.json | 1+
Apython/erc20_pool/quote.py | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/erc20_pool/unittest/quote.py | 37+++++++++++++++++++++++++++++++++++++
Apython/tests/test_quoter.py | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asolidity/DecimalQuote.sol | 39+++++++++++++++++++++++++++++++++++++++
Msolidity/Makefile | 4++++
9 files changed, 324 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,81 @@ +# erc20-pool + +A token pool implementation that allows _deposits_ in the form of _liquidity donations_, and _withdrawals_ in the form of _token swaps_. + +It satisfies the [CIC TokenSwap](https://git.grassecon.net/cicnet/cic-contracts/) interface. + + +## Synopsis + +| agent | action | agent change | pool change | +|---|---|---|---| +| alice | deposit(FOO, 1000000) | -1000000 FOO | +1000000 FOO | +| bob | withdraw(FOO, BAR, 1000) | -1000 BAR, +1000 FOO | +1000 BAR, -1000 FOO | +| alice | withdraw(BAR, FOO, 1000) | +1000 BAR, -1000 FOO | -1000 BAR, +1000 FOO | + + +## Publishing the contract + +There are six constructor arguments. + +The first three, `name`, `symbol` and `decimals` have matching getter methods, are analogous to the ERC20 methods of the same name. + +The `declaration` parameter is optional and can be any arbitrary content. Typically it defines a content hash of data describing the pool resource. A value of `bytes32(0x0)` means "no declaration defined." + +The `tokenRegistry` parameter takes an address to a smart contract controlling which tokens are allowed in the pool. See "Token approval" below. A value of `address(0x0)` deactivates this control, and allows the pool to hold all tokens by default (although they may still be subject to value limits). + +The `tokenLimiter` parameter takes an address to a smart contract controlling value limits of tokens in the pool. See "Token limits" below. A value of `address(0x0)` deactivates this control, and allows any value of (approved) tokens to be held by the pool. + + +### Token approval + +By specifying a non-zero contract address for the `tokenRegistry` property that implements the [CIC AccountsIndex]() interface, that contract can be used to allow and disallow which tokens can be used as input tokens to `deposit()` and `withdraw()`. + +Tokens that are disallowed while the pool still holds a balance can still be withdrawn in full. + + +### Token limits + +By specifying a non-zero contract address for the `tokenRegistry` property that implements the [CIC TokenLimit]() interface, that contract can be used to control the value limit allowed for each token in the pool. + +Tokens that are limited below the current balance held by the pool can still be withdrawn. Once the balance goes below the limit, additional tokens values may again be swapped, up to the limit. + + +#### Using limiter as registry + +The [erc20-limiter](https://holbrook.no/src/erc20-limiter/log.html) repository contains the smart contract implementation `LimiterIndex.sol`. This uses the token limit state to satisfy the [CIC AccountsIndex]() interface. Specifically, any token limit higher than 0 will be defined as allowed. + +This enables to publisher to use the same smart contract for both constructor arguments `tokenRegistry` and `tokenLimiter`. + + +## Handling values + +The pool contract does no checking whatsoever regarding the sanity of allowing tokens in the pool. + +It is therefore the responsibility of the maintainer of the list of allowed tokens to ensure that tokens in the pool are exchangeable in a sensible way. + +Some obvious concerns are: + +- Tokens are swapped denominated in their smallest unit (regardless of "decimals"). +- The unit of account of the tokens may differ. +- The value of the tokens in relation to unit of account may differ. + + +### Providing quotes + +Using the `setQuoter()` method, a smart contract address can be defined that translates value between tokens when exchanging tokens in the pool. + +The value returned from the "quoter" is the value of output tokens that will be received in return for the value of input tokens specified. + +The "quoter" smart contract must satisfy the [CIC TokenQuote]() interface. + +An example quoter contract `Quote.sol` can be found in this repository. The contract translates values according to the decimal count reported by the respective ERC20 tokens. + + +## Sealing the contract + +The contract implements the [CIC Seal]() interface for the following properties: + +- Fee value +- Fee address +- Quoter contract address diff --git a/python/erc20_pool/data/DecimalQuote.bin b/python/erc20_pool/data/DecimalQuote.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506108ed806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c010000000000000000000000000000000000000000000000000000000090048063dbb21d401461004d575b600080fd5b6100676004803603810190610062919061044b565b61007d565b60405161007491906104ad565b60405180910390f35b600080600080600060608873ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f313ce567000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161012e9190610539565b6000604051808303816000865af19150503d806000811461016b576040519150601f19603f3d011682016040523d82523d6000602084013e610170565b606091505b508092508193505050816101b9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101b0906105ad565b60405180910390fd5b808060200190518101906101cd9190610606565b94508773ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f313ce567000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516102769190610539565b6000604051808303816000865af19150503d80600081146102b3576040519150601f19603f3d011682016040523d82523d6000602084013e6102b8565b606091505b50809250819350505081610301576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102f8906105ad565b60405180910390fd5b808060200190518101906103159190610606565b93508460ff168460ff16036103315786955050505050506103ab565b8460ff168460ff16111561037757848461034b9190610662565b60ff16925082600a61035d91906107ca565b9250828761036b9190610844565b955050505050506103ab565b83856103839190610662565b60ff16925082600a61039591906107ca565b925082876103a39190610875565b955050505050505b9392505050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103e2826103b7565b9050919050565b6103f2816103d7565b81146103fd57600080fd5b50565b60008135905061040f816103e9565b92915050565b6000819050919050565b61042881610415565b811461043357600080fd5b50565b6000813590506104458161041f565b92915050565b600080600060608486031215610464576104636103b2565b5b600061047286828701610400565b935050602061048386828701610400565b925050604061049486828701610436565b9150509250925092565b6104a781610415565b82525050565b60006020820190506104c2600083018461049e565b92915050565b600081519050919050565b600081905092915050565b60005b838110156104fc5780820151818401526020810190506104e1565b60008484015250505050565b6000610513826104c8565b61051d81856104d3565b935061052d8185602086016104de565b80840191505092915050565b60006105458284610508565b915081905092915050565b600082825260208201905092915050565b7f4552525f544f4b454e0000000000000000000000000000000000000000000000600082015250565b6000610597600983610550565b91506105a282610561565b602082019050919050565b600060208201905081810360008301526105c68161058a565b9050919050565b600060ff82169050919050565b6105e3816105cd565b81146105ee57600080fd5b50565b600081519050610600816105da565b92915050565b60006020828403121561061c5761061b6103b2565b5b600061062a848285016105f1565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061066d826105cd565b9150610678836105cd565b9250828203905060ff81111561069157610690610633565b5b92915050565b6000600282049050919050565b6000808291508390505b60018511156106ee578086048111156106ca576106c9610633565b5b60018516156106d95780820291505b80810290506106e785610697565b94506106ae565b94509492505050565b60008261070757600190506107c3565b8161071557600090506107c3565b816001811461072b576002811461073557610764565b60019150506107c3565b60ff84111561074757610746610633565b5b8360020a91508482111561075e5761075d610633565b5b506107c3565b5060208310610133831016604e8410600b84101617156107995782820a90508381111561079457610793610633565b5b6107c3565b6107a684848460016106a4565b925090508184048111156107bd576107bc610633565b5b81810290505b9392505050565b60006107d582610415565b91506107e083610415565b925061080d7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84846106f7565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061084f82610415565b915061085a83610415565b92508261086a57610869610815565b5b828204905092915050565b600061088082610415565b915061088b83610415565b925082820261089981610415565b915082820484148315176108b0576108af610633565b5b509291505056fea26469706673582212201acbf661561a2312e8b64602d749a5af7f05876881796fccdca1072b6ee3bf6664736f6c63430008130033 +\ No newline at end of file diff --git a/python/erc20_pool/data/DecimalQuote.json b/python/erc20_pool/data/DecimalQuote.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_outToken","type":"address"},{"internalType":"address","name":"_inToken","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"valueFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}] diff --git a/python/erc20_pool/data/DecimalQuote.metadata.json b/python/erc20_pool/data/DecimalQuote.metadata.json @@ -0,0 +1 @@ +{"compiler":{"version":"0.8.19+commit.7dd6d404"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"_outToken","type":"address"},{"internalType":"address","name":"_inToken","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"valueFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"DecimalQuote.sol":"DecimalQuote"},"evmVersion":"byzantium","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"DecimalQuote.sol":{"keccak256":"0x9e5bc44ea381d6794c6bc992b8cee0b68fb700ab0ad888e5cec390d2261fd5f2","license":"AGPL-3.0-or-later","urls":["bzz-raw://a1d11d7ee1286203963eef4846272465fc7abdeb5c4e054f84fe9dada91b10ee","dweb:/ipfs/QmZQDttgfUBAJxxRpxW8JkwkWi61hNQ79NNrWD64Ak92vw"]}},"version":1} diff --git a/python/erc20_pool/quote.py b/python/erc20_pool/quote.py @@ -0,0 +1,98 @@ +# standard imports +import logging +import os +import enum + +# external imports +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.eth.constant import ZERO_CONTENT +from chainlib.eth.contract import ( + ABIContractEncoder, + ABIContractDecoder, + ABIContractType, + abi_decode_single, +) +from chainlib.eth.jsonrpc import to_blockheight_param +from chainlib.eth.error import RequestMismatchException +from chainlib.eth.tx import ( + TxFactory, + TxFormat, +) +from chainlib.jsonrpc import JSONRPCRequest +from chainlib.block import BlockSpec +from hexathon import ( + add_0x, + strip_0x, +) +from chainlib.eth.cli.encode import CLIEncoder + +# local imports +from erc20_pool.data import data_dir + +logg = logging.getLogger() + + +class DecimalQuote(TxFactory): + + __abi = None + __bytecode = None + + def constructor(self, sender_address, tx_format=TxFormat.JSONRPC, version=None): + code = self.cargs(version=version) + tx = self.template(sender_address, None, use_nonce=True) + tx = self.set_code(tx, code) + return self.finalize(tx, tx_format) + + + @staticmethod + def cargs(version=None): + code = DecimalQuote.bytecode(version=version) + enc = ABIContractEncoder() + args = enc.get() + code += args + logg.debug('constructor code: ' + args) + return code + + + @staticmethod + def gas(code=None): + return 4000000 + + + @staticmethod + def abi(): + if DecimalQuote.__abi == None: + f = open(os.path.join(data_dir, 'DecimalQuote.json'), 'r') + DecimalQuote.__abi = json.load(f) + f.close() + return DecimalQuote.__abi + + + @staticmethod + def bytecode(version=None): + if DecimalQuote.__bytecode == None: + f = open(os.path.join(data_dir, 'DecimalQuote.bin')) + DecimalQuote.__bytecode = f.read() + f.close() + return DecimalQuote.__bytecode + + + def value_for(self, contract_address, token_address_out, token_address_in, value_in, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('valueFor') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + enc.address(token_address_out) + enc.address(token_address_in) + enc.uint256(value_in) + 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 diff --git a/python/erc20_pool/unittest/quote.py b/python/erc20_pool/unittest/quote.py @@ -0,0 +1,37 @@ +# standard imports +import logging +import time +# external imports +from chainlib.eth.unittest.ethtester import EthTesterCase +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.tx import receipt +from chainlib.eth.address import to_checksum_address +from giftable_erc20_token.unittest import TestGiftableToken +from eth_erc20 import ERC20 +from chainlib.eth.block import block_latest +from eth_accounts_index.unittest import TestAccountsIndex +from eth_accounts_index.registry import AccountRegistry +from giftable_erc20_token import GiftableToken +from erc20_limiter.unittest import TestLimiter + +# local imports +from erc20_pool.quote import DecimalQuote + +logg = logging.getLogger(__name__) + +class TestDecimalQuote(EthTesterCase): + + def setUp(self): + super(TestDecimalQuote, self).setUp() + self.conn = RPCConnection.connect(self.chain_spec, 'default') + + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.conn) + c = DecimalQuote(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + (tx_hash, o) = c.constructor(self.accounts[0]) + self.rpc.do(o) + o = receipt(tx_hash) + r = self.rpc.do(o) + self.assertEqual(r['status'], 1) + self.quoter_address = to_checksum_address(r['contract_address']) + logg.debug('published quoter {}'.format(self.quoter_address)) diff --git a/python/tests/test_quoter.py b/python/tests/test_quoter.py @@ -0,0 +1,61 @@ +# standard imports +import unittest +import logging +import os +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.tx import receipt +from chainlib.eth.block import block_latest +from hexathon import same as same_hex +from eth_erc20 import ERC20 +from giftable_erc20_token import GiftableToken +from erc20_limiter import Limiter + +# local imports +from erc20_pool.quote import DecimalQuote +from erc20_pool.unittest.quote import TestDecimalQuote + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +class TestQuote(TestDecimalQuote): + + def setUp(self): + super(TestQuote, self).setUp() + self.tokens = {} + self.publish_token('Foo token', 'FOO', 14) + self.publish_token('Bar token', 'BAR', 18) + self.publish_token('Baz token', 'BAZ', 11) + + def publish_token(self, name, symbol, decimals): + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.conn) + c = GiftableToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + self.symbol = name + self.name = symbol + self.decimals = decimals + (tx_hash, o) = c.constructor(self.accounts[0], self.name, self.symbol, self.decimals, expire=0) + self.rpc.do(o) + o = receipt(tx_hash) + r = self.rpc.do(o) + self.assertEqual(r['status'], 1) + address = r['contract_address'] + logg.debug('published "{}" ("{}") on address {} with hash {}'.format(name, symbol, address, tx_hash)) + self.tokens[symbol] = address + + + def test_quote(self): + c = DecimalQuote(self.chain_spec) + o = c.value_for(self.quoter_address, self.tokens['FOO'], self.tokens['FOO'], 10**18, sender_address=self.accounts[0]) + r = self.rpc.do(o) + self.assertEqual(int(r, 16), 10**18) + + o = c.value_for(self.quoter_address, self.tokens['FOO'], self.tokens['BAR'], 10**18, sender_address=self.accounts[0]) + r = self.rpc.do(o) + self.assertEqual(int(r, 16), 10**14) + + o = c.value_for(self.quoter_address, self.tokens['FOO'], self.tokens['BAZ'], 10**18, sender_address=self.accounts[0]) + r = self.rpc.do(o) + self.assertEqual(int(r, 16), 10**21) + + +if __name__ == '__main__': + unittest.main() diff --git a/solidity/DecimalQuote.sol b/solidity/DecimalQuote.sol @@ -0,0 +1,39 @@ +pragma solidity ^0.8.0; + +// Author: Louis Holbrook <dev@holbrook.no> 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 +// SPDX-License-Identifier: AGPL-3.0-or-later +// File-Version: 1 +// Description: ACL-enabled ERC20 token swap for tokens with compatible properties. + +contract DecimalQuote { + // Implements TokenQuote + function valueFor(address _outToken, address _inToken, uint256 _value) public returns(uint256) { + uint8 dout; + uint8 din; + uint256 d; + bool r; + bytes memory v; + + (r, v) = _outToken.call(abi.encodeWithSignature("decimals()")); + require(r, "ERR_TOKEN"); + dout = abi.decode(v, (uint8)); + + (r, v) = _inToken.call(abi.encodeWithSignature("decimals()")); + require(r, "ERR_TOKEN"); + din = abi.decode(v, (uint8)); + + if (din == dout) { + return _value; + } + + if (din > dout) { + d = din - dout; + d = 10 ** d; + return _value / d; + } else { + d = dout - din; + d = 10 ** d; + return _value * d; + } + } +} diff --git a/solidity/Makefile b/solidity/Makefile @@ -5,6 +5,10 @@ all: $(SOLC) --abi SwapPool.sol --evm-version byzantium | awk 'NR>3' > SwapPool.json $(SOLC) --metadata SwapPool.sol --evm-version byzantium | awk 'NR>3' > SwapPool.metadata.json truncate -s -1 SwapPool.bin + $(SOLC) --bin DecimalQuote.sol --evm-version byzantium | awk 'NR>3' > DecimalQuote.bin + $(SOLC) --abi DecimalQuote.sol --evm-version byzantium | awk 'NR>3' > DecimalQuote.json + $(SOLC) --metadata DecimalQuote.sol --evm-version byzantium | awk 'NR>3' > DecimalQuote.metadata.json + truncate -s -1 DecimalQuote.bin install: all cp -v *.json ../python/erc20_pool/data/