erc20-limiter

ERC20 balance limit registry
Log | Files | Refs | README

commit 410ad0f0775a0ba64456584c54b02fef47bca364
parent 3bfe4ba642941b00c3468f7462f741400243eb45
Author: lash <dev@holbrook.no>
Date:   Fri, 28 Jul 2023 12:57:08 +0100

Add token registry adapter

Diffstat:
Apython/erc20_limiter/data/LimiterTokenRegistry.bin | 2++
Apython/erc20_limiter/data/LimiterTokenRegistry.json | 1+
Apython/erc20_limiter/data/LimiterTokenRegistry.metadata.json | 1+
Mpython/erc20_limiter/limiter.py | 4+++-
Apython/erc20_limiter/token.py | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpython/erc20_limiter/unittest/base.py | 16++++++++++++++++
Apython/run_tests.sh | 14++++++++++++++
Apython/tests/test_token_registry.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asolidity/LimiterTokenRegistry.sol | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msolidity/Makefile | 5+++++
10 files changed, 237 insertions(+), 1 deletion(-)

diff --git a/python/erc20_limiter/data/LimiterTokenRegistry.bin b/python/erc20_limiter/data/LimiterTokenRegistry.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506040516109673803806109678339818101604052810190610032919061015b565b81600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061019b565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100ea826100bf565b9050919050565b6100fa816100df565b811461010557600080fd5b50565b600081519050610117816100f1565b92915050565b6000610128826100df565b9050919050565b6101388161011d565b811461014357600080fd5b50565b6000815190506101558161012f565b92915050565b60008060408385031215610172576101716100ba565b5b600061018085828601610108565b925050602061019185828601610146565b9150509250929050565b6107bd806101aa6000396000f3fe608060405234801561001057600080fd5b5060043610610074576000357c01000000000000000000000000000000000000000000000000000000009004806301ffc9a71461007957806323778613146100a957806336db43b5146100d95780633ef25013146100f5578063bdd5544014610125575b600080fd5b610093600480360381019061008e91906104b5565b610141565b6040516100a091906104fd565b60405180910390f35b6100c360048036038101906100be9190610576565b6101f1565b6040516100d091906105cf565b60405180910390f35b6100f360048036038101906100ee9190610616565b6102b4565b005b61010f600480360381019061010a9190610656565b6102c3565b60405161011c91906104fd565b60405180910390f35b61013f600480360381019061013a9190610683565b6103a8565b005b60006301ffc9a77c010000000000000000000000000000000000000000000000000000000002827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19160361019557600190506101ec565b63b7bca6257c010000000000000000000000000000000000000000000000000000000002827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916036101e757600190506101ec565b600090505b919050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16632377861384846040518363ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040161026b9291906106e5565b602060405180830381865afa158015610288573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102ac9190610723565b905092915050565b6102bf8233836103a8565b5050565b60008060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16632377861384600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff166040518363ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040161035f9291906106e5565b602060405180830381865afa15801561037c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103a09190610723565b119050919050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663bdd554408484846040518463ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040161042193929190610750565b600060405180830381600087803b15801561043b57600080fd5b505af115801561044f573d6000803e3d6000fd5b50505050505050565b600080fd5b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b6104928161045d565b811461049d57600080fd5b50565b6000813590506104af81610489565b92915050565b6000602082840312156104cb576104ca610458565b5b60006104d9848285016104a0565b91505092915050565b60008115159050919050565b6104f7816104e2565b82525050565b600060208201905061051260008301846104ee565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061054382610518565b9050919050565b61055381610538565b811461055e57600080fd5b50565b6000813590506105708161054a565b92915050565b6000806040838503121561058d5761058c610458565b5b600061059b85828601610561565b92505060206105ac85828601610561565b9150509250929050565b6000819050919050565b6105c9816105b6565b82525050565b60006020820190506105e460008301846105c0565b92915050565b6105f3816105b6565b81146105fe57600080fd5b50565b600081359050610610816105ea565b92915050565b6000806040838503121561062d5761062c610458565b5b600061063b85828601610561565b925050602061064c85828601610601565b9150509250929050565b60006020828403121561066c5761066b610458565b5b600061067a84828501610561565b91505092915050565b60008060006060848603121561069c5761069b610458565b5b60006106aa86828701610561565b93505060206106bb86828701610561565b92505060406106cc86828701610601565b9150509250925092565b6106df81610538565b82525050565b60006040820190506106fa60008301856106d6565b61070760208301846106d6565b9392505050565b60008151905061071d816105ea565b92915050565b60006020828403121561073957610738610458565b5b60006107478482850161070e565b91505092915050565b600060608201905061076560008301866106d6565b61077260208301856106d6565b61077f60408301846105c0565b94935050505056fea264697066735822122059ed451c7493185b9c26a93c433ec67c46b6c713de36f371528416bd733499c964736f6c63430008130033 +\ No newline at end of file diff --git a/python/erc20_limiter/data/LimiterTokenRegistry.json b/python/erc20_limiter/data/LimiterTokenRegistry.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_holder","type":"address"},{"internalType":"contract Limiter","name":"_limiter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"have","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_holder","type":"address"}],"name":"limitOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_holder","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setLimitFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_sum","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}] diff --git a/python/erc20_limiter/data/LimiterTokenRegistry.metadata.json b/python/erc20_limiter/data/LimiterTokenRegistry.metadata.json @@ -0,0 +1 @@ +{"compiler":{"version":"0.8.19+commit.7dd6d404"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"_holder","type":"address"},{"internalType":"contract Limiter","name":"_limiter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"have","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_holder","type":"address"}],"name":"limitOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_holder","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setLimitFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_sum","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"LimiterTokenRegistry.sol":"LimiterTokenRegistry"},"evmVersion":"byzantium","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"LimiterTokenRegistry.sol":{"keccak256":"0xca5fd95fd62ac8530df26ea912eadfcb2c2a00bbf3df9ffe497d7a5c50b84497","license":"AGPL-3.0-or-later","urls":["bzz-raw://7d3e45e63f2e7fa721bd1f2c6a453926e5366bad8cfe3154323ee9d0f3476f5c","dweb:/ipfs/QmQDtyCrn7rf8LyxhrfxRd3K8CMuJuP1GL4YPyGAVETgtY"]}},"version":1} diff --git a/python/erc20_limiter/limiter.py b/python/erc20_limiter/limiter.py @@ -79,7 +79,9 @@ class Limiter(TxFactory): def set_limit(self, contract_address, sender_address, token_address, limit, holder_address=None, tx_format=TxFormat.JSONRPC, id_generator=None): enc = ABIContractEncoder() - if holder_address != None: + if holder_address == None: + enc.method('setLimit') + else: enc.method('setLimitFor') enc.typ(ABIContractType.ADDRESS) if holder_address != None: diff --git a/python/erc20_limiter/token.py b/python/erc20_limiter/token.py @@ -0,0 +1,96 @@ +# 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_limiter.data import data_dir + +logg = logging.getLogger() + + +class LimiterTokenRegistry(TxFactory): + + __abi = None + __bytecode = None + + def constructor(self, sender_address, holder_address, limiter_address, tx_format=TxFormat.JSONRPC, version=None): + code = self.cargs(holder_address, limiter_address, 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(holder_address, limiter_address, version=None): + code = LimiterTokenRegistry.bytecode(version=version) + enc = ABIContractEncoder() + enc.address(holder_address) + enc.address(limiter_address) + args = enc.get() + code += args + logg.debug('constructor code: ' + args) + return code + + + @staticmethod + def gas(code=None): + return 4000000 + + + @staticmethod + def abi(): + if LimiterTokenRegistry.__abi == None: + f = open(os.path.join(data_dir, 'LimiterTokenRegistry.json'), 'r') + LimiterTokenRegistry.__abi = json.load(f) + f.close() + return LimiterTokenRegistry.__abi + + + @staticmethod + def bytecode(version=None): + if LimiterTokenRegistry.__bytecode == None: + f = open(os.path.join(data_dir, 'LimiterTokenRegistry.bin')) + LimiterTokenRegistry.__bytecode = f.read() + f.close() + return LimiterTokenRegistry.__bytecode + + + def have(self, contract_address, token_address, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('have') + enc.typ(ABIContractType.ADDRESS) + enc.address(token_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 diff --git a/python/erc20_limiter/unittest/base.py b/python/erc20_limiter/unittest/base.py @@ -12,6 +12,7 @@ from chainlib.eth.block import block_latest # local imports from erc20_limiter import Limiter +from erc20_limiter.token import LimiterTokenRegistry logg = logging.getLogger(__name__) @@ -34,3 +35,18 @@ class TestLimiter(EthTesterCase): address = to_checksum_address(r['contract_address']) logg.debug('published limiter on address {} with hash {}'.format(address, tx_hash)) return address + + +class TestLimiterTokenRegistry(TestLimiter): + + def publish_token_registry(self, holder_address, limiter_address): + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.conn) + c = LimiterTokenRegistry(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + (tx_hash, o) = c.constructor(self.accounts[0], holder_address, limiter_address) + self.rpc.do(o) + o = receipt(tx_hash) + r = self.rpc.do(o) + self.assertEqual(r['status'], 1) + address = to_checksum_address(r['contract_address']) + logg.debug('published limiter token registry proxy on address {} with hash {}'.format(address, tx_hash)) + return address diff --git a/python/run_tests.sh b/python/run_tests.sh @@ -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/*.py`; do + python $f +done +set +x +set +e +set +a diff --git a/python/tests/test_token_registry.py b/python/tests/test_token_registry.py @@ -0,0 +1,51 @@ +# standard imports +import unittest +import logging + +# external imports +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.tx import receipt + +# local imports +from erc20_limiter import Limiter +from erc20_limiter.token import LimiterTokenRegistry +from erc20_limiter.unittest import TestLimiterTokenRegistry + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + + +class TestLimiterBase(TestLimiterTokenRegistry): + + def setUp(self): + super(TestLimiterBase, self).setUp() + self.publish_limiter() + self.token_registry = self.publish_token_registry(self.accounts[0], self.address) + logg.debug('tokenreg {}'.format(self.token_registry)) + + + def test_limit(self): + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.conn) + + foo_token = '2c26b46b68ffc68ff99b453c1d30413413422d70' + c = LimiterTokenRegistry(self.chain_spec) + o = c.have(self.token_registry, foo_token, sender_address=self.accounts[0]) + r = self.rpc.do(o) + self.assertEqual(int(r, 16), 0) + + c = Limiter(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + (tx_hash, o) = c.set_limit(self.address, self.accounts[0], foo_token, 42) + self.rpc.do(o) + o = receipt(tx_hash) + r = self.rpc.do(o) + self.assertEqual(r['status'], 1) + + c = LimiterTokenRegistry(self.chain_spec) + o = c.have(self.token_registry, foo_token, sender_address=self.accounts[0]) + r = self.rpc.do(o) + self.assertEqual(int(r, 16), 1) + + +if __name__ == '__main__': + unittest.main() + diff --git a/solidity/LimiterTokenRegistry.sol b/solidity/LimiterTokenRegistry.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.8.0; + +// Author: Louis Holbrook <dev@holbrook.no> 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 +// SPDX-License-Identifier: AGPL-3.0-or-later +// File-Version: 1 +// Description: Registry of allowed ERC20 balance limits per-token and per-holder. + +interface Limiter { + function limitOf(address,address) external view returns(uint256); + function setLimit(address,uint256) external; + function setLimitFor(address,address,uint256) external; +} + +contract LimiterTokenRegistry { + Limiter limiter; + address holder; + + constructor(address _holder, Limiter _limiter) { + holder = _holder; + limiter = _limiter; + } + + function limitOf(address _token, address _holder) public view returns (uint256) { + return limiter.limitOf(_token, _holder); + } + + function setLimit(address _token, uint256 _value) public { + setLimitFor(_token, msg.sender, _value); + } + + function setLimitFor(address _token, address _holder, uint256 _value) public { + limiter.setLimitFor(_token, _holder, _value); + } + + function have(address _token) public view returns(bool) { + return limiter.limitOf(_token, holder) > 0; + } + + function supportsInterface(bytes4 _sum) public pure returns (bool) { + if (_sum == 0x01ffc9a7) { // ERC165 + return true; + } + if (_sum == 0xb7bca625) { // AccountsIndex + return true; + } + return false; + } +} diff --git a/solidity/Makefile b/solidity/Makefile @@ -4,7 +4,12 @@ all: $(SOLC) --bin Limiter.sol --evm-version byzantium | awk 'NR>3' > Limiter.bin $(SOLC) --abi Limiter.sol --evm-version byzantium | awk 'NR>3' > Limiter.json $(SOLC) --metadata Limiter.sol --evm-version byzantium | awk 'NR>3' > Limiter.metadata.json + $(SOLC) --bin LimiterTokenRegistry.sol --evm-version byzantium | awk 'NR>7' > LimiterTokenRegistry.bin + $(SOLC) --abi LimiterTokenRegistry.sol --evm-version byzantium | awk 'NR>7' > LimiterTokenRegistry.json + $(SOLC) --metadata LimiterTokenRegistry.sol --evm-version byzantium | awk 'NR>7' > LimiterTokenRegistry.metadata.json + truncate -s -1 Limiter.bin + truncate -s -1 LimiterTokenRegistry.bin install: all cp -v *.json ../python/erc20_limiter/data/