├── tests ├── __init__.py ├── ape │ ├── __init__.py │ └── contracts │ │ ├── compound-v2 │ │ ├── FixedPriceOracle.sol │ │ ├── PriceOracle.sol │ │ ├── InterestRateModel.sol │ │ ├── CErc20Immutable.sol │ │ ├── EIP20Interface.sol │ │ ├── ComptrollerInterface.sol │ │ ├── EIP20NonStandardInterface.sol │ │ ├── WhitePaperInterestRateModel.sol │ │ ├── ErrorReporter.sol │ │ ├── CEther.sol │ │ ├── ComptrollerStorage.sol │ │ ├── Unitroller.sol │ │ ├── ExponentialNoError.sol │ │ ├── SafeMath.sol │ │ ├── CErc20.sol │ │ └── CTokenInterfaces.sol │ │ └── token │ │ ├── Token_SafeMath.sol │ │ └── Token.sol ├── web3client │ ├── __init__.py │ ├── test_erc20_client.py │ ├── test_base_client_poa.py │ ├── test_sign.py │ ├── test_base_client.py │ ├── test_dual_client.py │ ├── test_parse_tx.py │ ├── test_compound_v2_client.py │ └── test_rpc_log_middleware.py ├── web3factory │ ├── __init__.py │ ├── test_networks.py │ └── README.md └── conftest.py ├── src ├── web3client │ ├── py.typed │ ├── __init__.py │ ├── helpers │ │ ├── __init__.py │ │ ├── debug.py │ │ ├── subscribe.py │ │ ├── general.py │ │ └── tx.py │ ├── types.py │ ├── exceptions.py │ ├── erc20_client.py │ └── abi │ │ └── erc20.json ├── web3test │ ├── py.typed │ ├── __init__.py │ ├── ape │ │ ├── __init__.py │ │ ├── helpers │ │ │ ├── __init__.py │ │ │ ├── ape.py │ │ │ ├── token.py │ │ │ └── compound_v2.py │ │ └── fixtures │ │ │ ├── __init__.py │ │ │ ├── compound_v2.py │ │ │ ├── base.py │ │ │ └── erc20.py │ ├── web3client │ │ ├── __init__.py │ │ └── fixtures │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── erc20.py │ │ │ ├── dual.py │ │ │ └── compound_v2.py │ └── web3factory │ │ ├── __init__.py │ │ └── fixtures │ │ ├── __init__.py │ │ └── base.py └── web3factory │ ├── __init__.py │ ├── py.typed │ ├── types.py │ ├── factory.py │ ├── erc20_tokens.py │ └── networks.py ├── scripts ├── __init__.py ├── deploy_ceth.py └── deploy_ctst.py ├── ape-config.yaml ├── tox.ini ├── examples ├── get_latest_block.py ├── stream_pending_txs.py ├── get_nonce.py ├── stream_blocks.py ├── filter_blocks.py ├── filter_pending_txs.py ├── get_balance.py ├── get_token_balance.py └── stream_pending_txs_async.py ├── .gitignore ├── TODO.md ├── .pre-commit-config.yaml ├── .vscode └── settings.suggested.json ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3client/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ape/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3factory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3factory/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/ape/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/web3client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/web3factory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3client/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/ape/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/web3client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/web3factory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web3test/web3factory/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # Folder for ape scripts, useful for testing purposes. 2 | # Ignore. 3 | -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | contracts_folder: tests/ape/contracts 2 | plugins: 3 | - name: solidity 4 | - name: anvil 5 | - name: etherscan 6 | -------------------------------------------------------------------------------- /src/web3test/ape/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .compound_v2 import * # noqa 3 | from .erc20 import * # noqa 4 | -------------------------------------------------------------------------------- /src/web3test/web3client/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .compound_v2 import * # noqa 3 | from .dual import * # noqa 4 | from .erc20 import * # noqa 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Define shared state for all tests 3 | """ 4 | 5 | pytest_plugins = [ 6 | "web3test-ape", 7 | "web3test-web3client", 8 | "web3test-web3factory", 9 | ] 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4 4 | env_list = py{39,310,311} 5 | isolated_build = true 6 | 7 | [testenv] 8 | description = run tests 9 | setenv = 10 | PDM_IGNORE_SAVED_PYTHON="1" 11 | deps = pdm 12 | commands = 13 | pdm install --dev 14 | ape test tests --network ::foundry 15 | -------------------------------------------------------------------------------- /src/web3client/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable, Literal 2 | 3 | from web3.types import TxData 4 | 5 | SubscriptionType = Literal[ 6 | "newHeads", "newPendingTransactions", "logs", "alchemy_newPendingTransactions" 7 | ] 8 | 9 | SubscriptionCallback = Callable[[Any, SubscriptionType, TxData], None] 10 | AsyncSubscriptionCallback = Callable[[Any, SubscriptionType, TxData], Awaitable[None]] 11 | -------------------------------------------------------------------------------- /src/web3test/ape/helpers/ape.py: -------------------------------------------------------------------------------- 1 | import ape 2 | 3 | 4 | def get_contract_container(contract_name: str) -> ape.contracts.ContractContainer: 5 | """Get a contract container from the ape project, that can be used 6 | to deploy a contract instance.""" 7 | try: 8 | return getattr(ape.project, contract_name) 9 | except AttributeError: 10 | raise ValueError(f"Contract {contract_name} not found in ape project") 11 | -------------------------------------------------------------------------------- /examples/get_latest_block.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print the latest block 3 | """ 4 | from sys import argv 5 | 6 | from web3client.helpers.debug import pprintAttributeDict 7 | from web3client.helpers.general import secondOrNone 8 | from web3factory.factory import make_client 9 | 10 | network = secondOrNone(argv) or "eth" 11 | 12 | client = make_client(network) 13 | 14 | block = client.get_latest_block() 15 | 16 | print(f">>> Latest block") 17 | pprintAttributeDict(block) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | .env 11 | .history 12 | reports/ 13 | .DS_Store 14 | Desktop.ini 15 | 16 | # python tooling 17 | .tox 18 | .cache 19 | .pytest_cache 20 | .pdm-python 21 | __pypackages__/ 22 | .mypy_cache 23 | pypi-*/ 24 | 25 | # other 26 | .vscode/settings.json 27 | venv/ 28 | .venv/ 29 | tests/ape/.build 30 | examples/asyncio_example.py 31 | node_modules 32 | -------------------------------------------------------------------------------- /src/web3client/exceptions.py: -------------------------------------------------------------------------------- 1 | class Web3ClientException(BaseException): 2 | pass 3 | 4 | 5 | class ProviderNotSet(Web3ClientException): 6 | pass 7 | 8 | 9 | class MissingParameter(Web3ClientException): 10 | pass 11 | 12 | 13 | class TransactionTooExpensive(Web3ClientException): 14 | pass 15 | 16 | 17 | class NetworkNotFound(Web3ClientException): 18 | pass 19 | 20 | 21 | class Erc20TokenNotFound(Web3ClientException): 22 | pass 23 | 24 | 25 | class Erc20TokenNotUnique(Web3ClientException): 26 | pass 27 | -------------------------------------------------------------------------------- /examples/stream_pending_txs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print pending transactions obtained real-time from a websocket connection, 3 | using eth_subscribe 4 | """ 5 | from web3factory.factory import make_client 6 | from web3factory.networks import supported_networks 7 | 8 | network_names = ",".join([n["name"] for n in supported_networks]) 9 | network = input(f"Network ({network_names}): ") or None 10 | ws_rpc = input("WS RPC: ") or None 11 | 12 | client = make_client(network, node_uri=ws_rpc) 13 | 14 | client.subscribe(lambda tx, _, __: print(f"Pending tx: {tx}"), once=False) 15 | -------------------------------------------------------------------------------- /examples/get_nonce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print the number of transactions sent by the given address 3 | """ 4 | from sys import argv 5 | 6 | from web3client.helpers.general import secondOrNone 7 | from web3factory.factory import make_client 8 | 9 | network = secondOrNone(argv) or "eth" 10 | 11 | client = make_client(network) 12 | 13 | address = input("Ethereum address? ") 14 | 15 | if len(address.replace("0x", "")) != 40: 16 | raise Exception("Please provide a valid Ethereum address") 17 | 18 | nonce = client.get_nonce(address) 19 | 20 | print(f">>> Transactions sent by {address}") 21 | print(nonce) 22 | -------------------------------------------------------------------------------- /examples/stream_blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print new blocks obtained real-time from a websocket connection, using eth_subscribe 3 | """ 4 | from web3factory.factory import make_client 5 | from web3factory.networks import supported_networks 6 | 7 | network_names = ",".join([n["name"] for n in supported_networks]) 8 | network = input(f"Network ({network_names}): ") or None 9 | ws_rpc = input("WS RPC: ") or None 10 | 11 | client = make_client(network, node_uri=ws_rpc) 12 | 13 | client.subscribe( 14 | lambda block, _, __: print(f"New block: {block}"), 15 | subscription_type="newHeads", 16 | once=False, 17 | ) 18 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/FixedPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./PriceOracle.sol"; 5 | 6 | contract FixedPriceOracle is PriceOracle { 7 | uint public price; 8 | 9 | constructor(uint _price) { 10 | price = _price; 11 | } 12 | 13 | function getUnderlyingPrice(CToken cToken) override public view returns (uint) { 14 | cToken; 15 | return price; 16 | } 17 | 18 | function assetPrices(address asset) public view returns (uint) { 19 | asset; 20 | return price; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/filter_blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print new blocks obtained with a polling mechanism, using eth_newBlockFilter 3 | """ 4 | from time import sleep 5 | 6 | from web3factory.factory import make_client 7 | from web3factory.networks import supported_networks 8 | 9 | network_names = ",".join([n["name"] for n in supported_networks]) 10 | network = input(f"Network ({network_names}): ") or None 11 | ws_rpc = input("Optionally specify RPC: ") or None 12 | 13 | client = make_client(network, node_uri=ws_rpc) 14 | 15 | filter = client.w3.eth.filter("latest") 16 | 17 | while True: 18 | for block in filter.get_new_entries(): 19 | print(f"New block: {block!r}") 20 | sleep(1) 21 | -------------------------------------------------------------------------------- /examples/filter_pending_txs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print new blocks obtained with a polling mechanism, using eth_newBlockFilter 3 | """ 4 | from time import sleep 5 | 6 | from web3factory.factory import make_client 7 | from web3factory.networks import supported_networks 8 | 9 | network_names = ",".join([n["name"] for n in supported_networks]) 10 | network = input(f"Network ({network_names}): ") or None 11 | ws_rpc = input("Optionally specify RPC: ") or None 12 | 13 | client = make_client(network, node_uri=ws_rpc) 14 | 15 | filter = client.w3.eth.filter("pending") 16 | 17 | while True: 18 | for tx in filter.get_new_entries(): 19 | print(f"New pending tx: {tx}") 20 | sleep(1) 21 | -------------------------------------------------------------------------------- /src/web3test/web3client/fixtures/base.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from web3client.base_client import BaseClient 6 | 7 | # Base client 8 | 9 | 10 | @pytest.fixture() 11 | def base_client(ape_chain_uri: str) -> BaseClient: 12 | return BaseClient(node_uri=ape_chain_uri) 13 | 14 | 15 | @pytest.fixture() 16 | def alice_base_client(accounts_keys: List[str], ape_chain_uri: str) -> BaseClient: 17 | return BaseClient(node_uri=ape_chain_uri, private_key=accounts_keys[0]) 18 | 19 | 20 | @pytest.fixture() 21 | def bob_base_client(accounts_keys: List[str], ape_chain_uri: str) -> BaseClient: 22 | return BaseClient(node_uri=ape_chain_uri, private_key=accounts_keys[1]) 23 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/PriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./CToken.sol"; 5 | 6 | abstract contract PriceOracle { 7 | /// @notice Indicator that this is a PriceOracle contract (for inspection) 8 | bool public constant isPriceOracle = true; 9 | 10 | /** 11 | * @notice Get the underlying price of a cToken asset 12 | * @param cToken The cToken to get the underlying price of 13 | * @return The underlying asset price mantissa (scaled by 1e18). 14 | * Zero means the price is unavailable. 15 | */ 16 | function getUnderlyingPrice(CToken cToken) virtual external view returns (uint); 17 | } 18 | -------------------------------------------------------------------------------- /examples/get_balance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Return the balance in ETH (or AVAX, BNB, etc, depending on 3 | the network) of the given address 4 | """ 5 | from sys import argv 6 | 7 | from web3client.helpers.general import secondOrNone, thirdOrNone 8 | from web3factory.factory import make_client 9 | from web3factory.networks import get_network_config 10 | 11 | address = secondOrNone(argv) 12 | if not address: 13 | raise Exception("Please give me a user address") 14 | 15 | network = thirdOrNone(argv) or "eth" 16 | 17 | client = make_client(network) 18 | 19 | balance_in_eth = client.get_balance_in_eth(address) 20 | 21 | print(f">>> Balance of {address} on {network}") 22 | print(f"{balance_in_eth} {get_network_config(network)['coin']}") 23 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Top priority 2 | 3 | - Why not using the networks defined in eth_utils/__json/eth_networks.json? 4 | 5 | # To do 6 | 7 | # Backlog 8 | 9 | - Test new Compound methods: borrow_fraction, repay_fraction, approve_and_repay_fraction, withdraw_fraction 10 | - Make chain_id, tx_type (and maybe more) cached properties of BaseClient 11 | - Cache supports_eip1559() and/or infer_eip1559() in BaseClient 12 | - Merge with [web3core](https://github.com/coccoinomane/web3cli/tree/master/src/web3core)? 13 | - Add Uniswap V2 LP contracts 14 | - Retry until a certain condition is met (via callback) 15 | - Retry: Implement 'retry until gas is low enough' 16 | - Subscribe: There can be many logs per transaction. Make sure you cache the tx data to avoid fetching it multiple times. 17 | -------------------------------------------------------------------------------- /tests/ape/contracts/token/Token_SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library Token_SafeMath { 4 | 5 | function add(uint a, uint b) internal pure returns (uint c) { 6 | c = a + b; 7 | require(c >= a); 8 | return c; 9 | } 10 | 11 | function sub(uint a, uint b) internal pure returns (uint c) { 12 | require(b <= a); 13 | c = a - b; 14 | return c; 15 | } 16 | 17 | function mul(uint a, uint b) internal pure returns (uint c) { 18 | c = a * b; 19 | require(a == 0 || c / a == b); 20 | return c; 21 | } 22 | 23 | function div(uint a, uint b) internal pure returns (uint c) { 24 | require(b > 0); 25 | c = a / b; 26 | return c; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/web3factory/types.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | from typing_extensions import NotRequired 4 | from web3.types import Middleware 5 | 6 | NetworkName = str 7 | TokenName = str 8 | 9 | 10 | class NetworkConfig(TypedDict): 11 | """ 12 | Dictionary representing the configuration of a network, e.g. Ethereum, 13 | Avalanche, etc. 14 | """ 15 | 16 | name: NetworkName 17 | tx_type: int 18 | chain_id: int 19 | middlewares: NotRequired[List[Middleware]] 20 | rpcs: NotRequired[List[str]] 21 | coin: str 22 | 23 | 24 | class Erc20TokenConfig(TypedDict): 25 | """ 26 | Dictionary representing an ERC20 token 27 | """ 28 | 29 | name: TokenName 30 | network: NetworkName 31 | address: str 32 | decimals: int 33 | -------------------------------------------------------------------------------- /examples/get_token_balance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Return the token balance of the given address 3 | """ 4 | from sys import argv 5 | 6 | from web3client.helpers.general import fourthOrNone, secondOrNone, thirdOrNone 7 | from web3factory.erc20_tokens import get_token_config 8 | from web3factory.factory import make_erc20_client 9 | 10 | address = secondOrNone(argv) 11 | if not address: 12 | raise Exception("Please give me a user address") 13 | 14 | token = thirdOrNone(argv) or "USDC" 15 | 16 | network = fourthOrNone(argv) or "eth" 17 | 18 | client = make_erc20_client(token, network) 19 | 20 | balance = client.balanceOf(address) 21 | balance_in_eth = client.from_wei(balance, get_token_config(token, network)["decimals"]) 22 | 23 | print(f">>> Balance of {address} on {network}") 24 | print(f"{balance_in_eth} {token}") 25 | -------------------------------------------------------------------------------- /examples/stream_pending_txs_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print pending transactions obtained real-time from a websocket connection, 3 | using eth_subscribe 4 | """ 5 | import asyncio 6 | 7 | from web3.types import TxData 8 | 9 | from web3factory.factory import make_client 10 | from web3factory.networks import supported_networks 11 | 12 | network_names = ",".join([n["name"] for n in supported_networks]) 13 | network = input(f"Network ({network_names}): ") or None 14 | ws_rpc = input("WS RPC: ") or None 15 | 16 | client = make_client(network, node_uri=ws_rpc) 17 | 18 | 19 | async def callback(tx: str, subscription_type: str, tx_data: TxData) -> None: 20 | print(f"Pending tx: {tx}") 21 | # Process transaction as you see fit... 22 | await asyncio.sleep(3) 23 | print(f" > Finished processing tx {tx}") 24 | 25 | 26 | asyncio.run(client.async_subscribe(callback, once=False)) 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.13.2 4 | hooks: 5 | - id: isort 6 | 7 | - repo: https://github.com/PyCQA/autoflake 8 | rev: v2.3.0 9 | hooks: 10 | - id: autoflake 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 24.2.0 14 | hooks: 15 | - id: black 16 | name: black 17 | 18 | ### For mypy, pre-commit ends up giving a different result than simply 19 | ### running `.venv/bin/mypy src tests`. So, we disabled it. 20 | - repo: https://github.com/pre-commit/mirrors-mypy 21 | rev: v1.8.0 22 | hooks: 23 | - id: mypy 24 | additional_dependencies: [mypy-extensions, types-setuptools, types-requests, typing_extensions, eth-typing] 25 | 26 | - repo: https://github.com/abravalheri/validate-pyproject 27 | rev: v0.10.1 28 | hooks: 29 | - id: validate-pyproject 30 | 31 | default_language_version: 32 | python: python3 -------------------------------------------------------------------------------- /src/web3test/web3factory/fixtures/base.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import pytest 4 | 5 | from web3client.base_client import BaseClient 6 | from web3factory.factory import make_client 7 | from web3factory.networks import supported_networks 8 | 9 | 10 | @pytest.fixture() 11 | def rpcs() -> Dict[str, str]: 12 | """ 13 | Let's use difrerent RPCs for tests, in case the regular ones 14 | are throttled 15 | """ 16 | return { 17 | "eth": "https://mainnet.infura.io/v3/db0e363aad2f43ee8a2f259733721512", 18 | } 19 | 20 | 21 | @pytest.fixture() 22 | def networks_clients(rpcs: Dict[str, str]) -> Dict[str, BaseClient]: 23 | """ 24 | Ready-to-use clients, indexed by network name, no signer 25 | """ 26 | clients = {} 27 | for network in supported_networks: 28 | name = network["name"] 29 | node_uri = rpcs.get(name) 30 | clients[name] = make_client(name, node_uri) 31 | return clients 32 | -------------------------------------------------------------------------------- /src/web3test/ape/helpers/token.py: -------------------------------------------------------------------------------- 1 | import ape 2 | 3 | 4 | def deploy_token( 5 | accounts: ape.managers.accounts.AccountManager, 6 | token_container: ape.contracts.ContractContainer, 7 | name: str = "Test Token", 8 | symbol: str = "TST", 9 | decimals: int = 18, 10 | initial_supply: int = 10000, 11 | distribute: bool = True, 12 | ) -> ape.contracts.ContractInstance: 13 | """Deploy a test token, and distribute it to all accounts. The token 14 | will be deployed by alice (accounts[0]) and optionally distributed 15 | to all other accounts.""" 16 | token = token_container.deploy( 17 | name, symbol, decimals, initial_supply * 10**decimals, sender=accounts[0] 18 | ) 19 | if distribute: 20 | for account in accounts[1:]: 21 | token.transfer( 22 | account, 23 | int(initial_supply / len(accounts)) * 10**decimals, 24 | sender=accounts[0], 25 | ) 26 | return token 27 | -------------------------------------------------------------------------------- /src/web3test/web3client/fixtures/erc20.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.erc20_client import Erc20Client 7 | 8 | 9 | @pytest.fixture() 10 | def erc20_client( 11 | TST: ape.contracts.ContractInstance, ape_chain_uri: str 12 | ) -> Erc20Client: 13 | return Erc20Client(node_uri=ape_chain_uri, contract_address=TST.address) 14 | 15 | 16 | @pytest.fixture() 17 | def alice_erc20_client( 18 | TST: ape.contracts.ContractInstance, accounts_keys: List[str], ape_chain_uri: str 19 | ) -> Erc20Client: 20 | return Erc20Client( 21 | node_uri=ape_chain_uri, 22 | contract_address=TST.address, 23 | private_key=accounts_keys[0], 24 | ) 25 | 26 | 27 | @pytest.fixture() 28 | def bob_erc20_client( 29 | TST: ape.contracts.ContractInstance, accounts_keys: List[str], ape_chain_uri: str 30 | ) -> Erc20Client: 31 | return Erc20Client( 32 | node_uri=ape_chain_uri, 33 | contract_address=TST.address, 34 | private_key=accounts_keys[1], 35 | ) 36 | -------------------------------------------------------------------------------- /scripts/deploy_ceth.py: -------------------------------------------------------------------------------- 1 | from ape import accounts, project 2 | 3 | 4 | def main() -> None: 5 | """Deploy Compound cETH money market. Refer to deploy_ctst 6 | for more detailed comments.""" 7 | alice = accounts.test_accounts[0] 8 | comptroller = project.Comptroller.deploy(sender=alice) 9 | ir_model = project.WhitePaperInterestRateModel.deploy( 10 | 0, 200000000000000000, sender=alice 11 | ) 12 | ceth = project.CEther.deploy( 13 | comptroller, 14 | ir_model, 15 | 200000000000000000000000000, 16 | "CompoundEther", 17 | "cETH", 18 | 8, 19 | alice, 20 | sender=alice, 21 | ) 22 | comptroller._supportMarket(ceth, sender=alice) 23 | ceth.mint(sender=alice, value=100 * 10**18) 24 | oracle = project.FixedPriceOracle.deploy(10**18, sender=alice) 25 | comptroller._setPriceOracle(oracle, sender=alice) 26 | comptroller._setCollateralFactor(ceth, 9 * 10**17, sender=alice) 27 | comptroller.enterMarkets([ceth], sender=alice) 28 | ceth.borrow(10 * 10**18, sender=alice) 29 | -------------------------------------------------------------------------------- /.vscode/settings.suggested.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[solidity]": { 4 | "editor.formatOnSave": false 5 | }, 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": true 8 | }, 9 | "files.exclude": { 10 | "**/__pycache__": true, 11 | "**/.pytest_cache": true, 12 | "**/.mypy_cache": true, 13 | "pypi-*/": true 14 | }, 15 | "python.analysis.typeCheckingMode": "off", 16 | "python.languageServer": "Pylance", 17 | "python.analysis.autoImportCompletions": true, 18 | "rewrap.autoWrap.enabled": true, 19 | "rewrap.wrappingColumn": 80, 20 | "python.testing.pytestArgs": [ 21 | "tests" 22 | ], 23 | "python.testing.unittestEnabled": false, 24 | "python.testing.pytestEnabled": true, 25 | "solidity.enabledAsYouTypeCompilationErrorCheck": false, 26 | "black-formatter.importStrategy": "fromEnvironment", 27 | "mypy-type-checker.importStrategy": "fromEnvironment", 28 | "python.terminal.activateEnvInCurrentTerminal": true, 29 | "mypy-type-checker.path": [ 30 | ".venv/bin/mypy" 31 | ], 32 | "mypy-type-checker.interpreter": [ 33 | ".venv/bin/python" 34 | ] 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 coccoinomane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/web3test/web3client/fixtures/dual.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.erc20_client import DualClient 7 | 8 | 9 | @pytest.fixture() 10 | def dual_client_token( 11 | TST: ape.contracts.ContractInstance, ape_chain_uri: str 12 | ) -> DualClient: 13 | return DualClient(node_uri=ape_chain_uri, contract_address=TST.address) 14 | 15 | 16 | @pytest.fixture() 17 | def dual_client_native(ape_chain_uri: str) -> DualClient: 18 | return DualClient(node_uri=ape_chain_uri, contract_address="native") 19 | 20 | 21 | @pytest.fixture() 22 | def alice_dual_client_token( 23 | TST: ape.contracts.ContractInstance, accounts_keys: List[str], ape_chain_uri: str 24 | ) -> DualClient: 25 | return DualClient( 26 | node_uri=ape_chain_uri, 27 | contract_address=TST.address, 28 | private_key=accounts_keys[0], 29 | ) 30 | 31 | 32 | @pytest.fixture() 33 | def alice_dual_client_native( 34 | accounts_keys: List[str], ape_chain_uri: str 35 | ) -> DualClient: 36 | return DualClient( 37 | node_uri=ape_chain_uri, 38 | contract_address="native", 39 | private_key=accounts_keys[0], 40 | ) 41 | -------------------------------------------------------------------------------- /tests/web3factory/test_networks.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import pytest 4 | 5 | from web3client.base_client import BaseClient 6 | 7 | ethereum_foundation = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" 8 | 9 | 10 | @pytest.mark.remote 11 | def test_get_nonce(networks_clients: Dict[str, BaseClient]) -> None: 12 | for network, client in networks_clients.items(): 13 | nonce = client.get_nonce(ethereum_foundation) 14 | assert type(nonce) is int 15 | assert nonce >= 0 16 | 17 | 18 | @pytest.mark.remote 19 | def test_get_latest_block(networks_clients: Dict[str, BaseClient]) -> None: 20 | for network, client in networks_clients.items(): 21 | block = client.get_latest_block() 22 | assert type(block.get("number")) is int 23 | assert block.get("number") >= 0 24 | assert type(block.get("size")) is int 25 | assert block.get("size") >= 0 26 | assert type(block.get("difficulty")) is int 27 | assert block.get("difficulty") >= 0 28 | assert type(block.get("transactions")) is list 29 | 30 | 31 | @pytest.mark.remote 32 | def test_get_eth_balance(networks_clients: Dict[str, BaseClient]) -> None: 33 | for network, client in networks_clients.items(): 34 | balance = client.get_balance_in_wei(ethereum_foundation) 35 | assert type(balance) is int 36 | assert balance >= 0 37 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/InterestRateModel.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | /** 5 | * @title Compound's InterestRateModel Interface 6 | * @author Compound 7 | */ 8 | abstract contract InterestRateModel { 9 | /// @notice Indicator that this is an InterestRateModel contract (for inspection) 10 | bool public constant isInterestRateModel = true; 11 | 12 | /** 13 | * @notice Calculates the current borrow interest rate per block 14 | * @param cash The total amount of cash the market has 15 | * @param borrows The total amount of borrows the market has outstanding 16 | * @param reserves The total amount of reserves the market has 17 | * @return The borrow rate per block (as a percentage, and scaled by 1e18) 18 | */ 19 | function getBorrowRate(uint cash, uint borrows, uint reserves) virtual external view returns (uint); 20 | 21 | /** 22 | * @notice Calculates the current supply interest rate per block 23 | * @param cash The total amount of cash the market has 24 | * @param borrows The total amount of borrows the market has outstanding 25 | * @param reserves The total amount of reserves the market has 26 | * @param reserveFactorMantissa The current reserve factor the market has 27 | * @return The supply rate per block (as a percentage, and scaled by 1e18) 28 | */ 29 | function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual external view returns (uint); 30 | } 31 | -------------------------------------------------------------------------------- /scripts/deploy_ctst.py: -------------------------------------------------------------------------------- 1 | from ape import accounts, project 2 | 3 | 4 | def main() -> None: 5 | """Deploy Compound cTST money market, where TST 6 | is a test token""" 7 | alice = accounts.test_accounts[0] 8 | 9 | # Deploy underlying token, comptroller, interest rate model, and cTST 10 | tst = project.Token.deploy( 11 | "Test token", "TST", 18, 10**9 * 10**18, sender=alice 12 | ) 13 | comptroller = project.Comptroller.deploy(sender=alice) 14 | ir_model = project.WhitePaperInterestRateModel.deploy( 15 | 0, 200000000000000000, sender=alice 16 | ) 17 | ctst = project.CErc20Immutable.deploy( 18 | tst, 19 | comptroller, 20 | ir_model, 21 | 200000000000000000000000000, 22 | "CompoundTST", 23 | "cTST", 24 | 8, 25 | alice, 26 | sender=alice, 27 | ) 28 | 29 | # List cTST market in the comptroller 30 | comptroller._supportMarket(ctst, sender=alice) 31 | 32 | # Alice supplies 100 TST to the Compound cTST market 33 | tst.approve(ctst, 100 * 10**18, sender=alice) 34 | ctst.mint(100 * 10**18, sender=alice) 35 | 36 | # All assets will have price of 1 ETH 37 | oracle = project.FixedPriceOracle.deploy(10**18, sender=alice) 38 | comptroller._setPriceOracle(oracle, sender=alice) 39 | 40 | # Set colletaral factor to 90% 41 | comptroller._setCollateralFactor(ctst, 9 * 10**17, sender=alice) 42 | 43 | # Alice borrows 10 TST from the Compound cTST market 44 | comptroller.enterMarkets([ctst], sender=alice) 45 | ctst.borrow(10 * 10**18, sender=alice) 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Pull requests are welcome ❤️ To start working on `web3client`, please follow these steps. 2 | 3 | # 1. Clone the repo 4 | 5 | This is simple: 6 | 7 | ```bash 8 | git clone https://github.com/coccoinomane/web3client.git 9 | ``` 10 | 11 | # 2. Install dependencies 12 | 13 | `web3client` uses [PDM](https://github.com/pdm-project/pdm/) to manage dependencies. You can install it via script: 14 | 15 | ```bash 16 | curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 - 17 | ``` 18 | 19 | Or, if you are on Windows, you may be better off using [pipx](https://pypa.github.io/pipx/): 20 | 21 | ```bash 22 | pipx install pdm 23 | ``` 24 | 25 | Then, to install `web3client` and its dependencies, just run: 26 | 27 | ```bash 28 | pdm install 29 | ``` 30 | 31 | All packages, including `web3client` will be installed in a newly created virtual environment in the `.venv` folder. 32 | 33 | # 3. Code! 34 | 35 | `web3client` consists of two main parts: 36 | 37 | - The client itself , in the [`src/web3client/`](./src/web3client/) folder. 38 | - The factory, in the [`src/web3factory/`](src/web3factory/) folder. 39 | 40 | # 4. Run tests 41 | 42 | When you are done with your changes, please make sure to run `pdm test` before 43 | committing to make sure that your code does not break anything. 44 | 45 | Some tests require a local blockchain, so make sure to install 46 | [ganache](https://www.npmjs.com/package/ganache) the first time you run `pdm 47 | test`! 48 | 49 | To create a new test, feel free to copy and customize an existing one: all tests 50 | are in the [`tests`](./tests) folder. 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/CErc20Immutable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./CErc20.sol"; 5 | 6 | /** 7 | * @title Compound's CErc20Immutable Contract 8 | * @notice CTokens which wrap an EIP-20 underlying and are immutable 9 | * @author Compound 10 | */ 11 | contract CErc20Immutable is CErc20 { 12 | /** 13 | * @notice Construct a new money market 14 | * @param underlying_ The address of the underlying asset 15 | * @param comptroller_ The address of the Comptroller 16 | * @param interestRateModel_ The address of the interest rate model 17 | * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 18 | * @param name_ ERC-20 name of this token 19 | * @param symbol_ ERC-20 symbol of this token 20 | * @param decimals_ ERC-20 decimal precision of this token 21 | * @param admin_ Address of the administrator of this token 22 | */ 23 | constructor(address underlying_, 24 | ComptrollerInterface comptroller_, 25 | InterestRateModel interestRateModel_, 26 | uint initialExchangeRateMantissa_, 27 | string memory name_, 28 | string memory symbol_, 29 | uint8 decimals_, 30 | address payable admin_) { 31 | // Creator of the contract is admin during initialization 32 | admin = payable(msg.sender); 33 | 34 | // Initialize the market 35 | initialize(underlying_, comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); 36 | 37 | // Set the proper admin now that initialization is done 38 | admin = admin_; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/web3factory/README.md: -------------------------------------------------------------------------------- 1 | Factory sub-module of `web3client`. 2 | 3 | # Examples 4 | 5 | Get the latest block on supported blockchains: 6 | 7 | ```python 8 | from web3factory.factory import make_client 9 | 10 | eth_block = make_client("eth").get_latest_block() # Ethereum 11 | bnb_block = make_client("bnb").get_latest_block() # BNB chain 12 | avax_block = make_client("avax").get_latest_block() # Avalanche 13 | arb_block = make_client("arb").get_latest_block() # Arbitrum 14 | era_block = make_client("era").get_latest_block() # zkSync Era 15 | ``` 16 | 17 | Get the ETH and USDC balances of the Ethereum foundation: 18 | 19 | ```python 20 | from web3factory.factory import make_client, make_erc20_client 21 | 22 | address = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" 23 | eth = make_client("eth").get_balance_in_eth(address) 24 | usdc = make_erc20_client("USDC", "eth").balanceOf(address) / 10**6 25 | ``` 26 | 27 | Get the BNB and BUSD balances of Binance's hot wallet: 28 | 29 | ```python 30 | from web3factory.factory import make_client, make_erc20_client 31 | 32 | address = "0x8894e0a0c962cb723c1976a4421c95949be2d4e3" 33 | bnb = make_client("bnb").get_balance_in_eth(address) 34 | busd = make_erc20_client("BUSD", "bnb").balanceOf(address) / 10**18 35 | ``` 36 | 37 | # Custom chains & contracts 38 | 39 | The factory module only allows to interact with a small list of chains and 40 | contracts. 41 | 42 | To interact with an arbitrary EVM chain or smart contract, instantiate a custom 43 | client using the [`BaseClient`](./src/web3client/base_client.py) class. 44 | 45 | For a more structured approach, use `web3core`, a sub-package 46 | of [`web3cli`](./src/web3cli/) that comes with many preloaded chains, and allows 47 | to import chains and smart contracts dynamically. 48 | 49 | -------------------------------------------------------------------------------- /tests/web3client/test_erc20_client.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.erc20_client import Erc20Client 7 | 8 | 9 | @pytest.mark.local 10 | def test_erc20_client_balance_in_wei( 11 | TST: ape.contracts.ContractInstance, 12 | erc20_client: Erc20Client, 13 | alice: ape.api.AccountAPI, 14 | ) -> None: 15 | alice_balance_in_wei = erc20_client.balance_in_wei(alice.address) 16 | assert TST.balanceOf(alice) == alice_balance_in_wei 17 | 18 | 19 | @pytest.mark.local 20 | def test_erc20_client_transfer( 21 | TST: ape.contracts.ContractInstance, 22 | alice_erc20_client: Erc20Client, 23 | bob: ape.api.AccountAPI, 24 | ) -> None: 25 | bob_balance = TST.balanceOf(bob) 26 | alice_erc20_client.transfer(bob.address, 10**18) 27 | assert TST.balanceOf(bob) == bob_balance + 10**18 28 | 29 | 30 | @pytest.mark.local 31 | def test_erc20_client_transfer_non_checksum_address( 32 | TST: ape.contracts.ContractInstance, 33 | alice_erc20_client: Erc20Client, 34 | bob: ape.api.AccountAPI, 35 | ) -> None: 36 | bob_balance = TST.balanceOf(bob) 37 | alice_erc20_client.transfer(str(bob.address).lower(), 10**18) 38 | assert TST.balanceOf(bob) == bob_balance + 10**18 39 | 40 | 41 | def test_erc20_client_clone( 42 | alice_erc20_client: Erc20Client, 43 | TST_0: ape.contracts.ContractInstance, 44 | ) -> None: 45 | clone = cast(Erc20Client, alice_erc20_client.clone(base=Erc20Client)) 46 | # The clone's contract must be the same as the original's 47 | assert clone.contract_address == alice_erc20_client.contract_address 48 | assert clone.abi == alice_erc20_client.abi 49 | # Setting a property on the clone must not change the original 50 | old_contract_address = alice_erc20_client.contract_address 51 | clone.set_contract(TST_0.address) 52 | assert alice_erc20_client.contract_address == old_contract_address 53 | assert clone.contract_address == TST_0.address 54 | -------------------------------------------------------------------------------- /src/web3factory/factory.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Type, cast 2 | 3 | from web3client.base_client import BaseClient 4 | from web3client.erc20_client import Erc20Client 5 | from web3factory.erc20_tokens import get_token_config 6 | from web3factory.networks import get_network_config, pick_first_rpc 7 | from web3factory.types import NetworkName, TokenName 8 | 9 | 10 | def make_client( 11 | network_name: NetworkName, 12 | node_uri: str = None, 13 | base: Type[BaseClient] = BaseClient, 14 | **clientArgs: Any, 15 | ) -> BaseClient: 16 | """ 17 | Return a brand new client configured for the given blockchain 18 | """ 19 | networkConfig = get_network_config(network_name) 20 | if node_uri is None: 21 | node_uri = pick_first_rpc(network_name) 22 | client = base(node_uri=node_uri, **clientArgs) 23 | client.chain_id = networkConfig["chain_id"] 24 | client.tx_type = networkConfig["tx_type"] 25 | client.set_middlewares(networkConfig.get("middlewares", [])) 26 | return client 27 | 28 | 29 | def make_erc20_client( 30 | networkName: NetworkName, 31 | node_uri: str = None, 32 | token_address: str = None, 33 | token_name: TokenName = None, 34 | **clientArgs: Any, 35 | ) -> Erc20Client: 36 | """ 37 | Return a brand new client configured for the given blockchain 38 | and preloaded with the ERC20 token ABI. 39 | 40 | You can specify the token address or the token name. In the latter case, 41 | the address will be fetched from the token list in erc20_tokens.py. 42 | """ 43 | if token_address is None and token_name is None: 44 | raise ValueError("You must specify either token_address or token_name") 45 | if token_address is None: 46 | token_address = get_token_config(token_name, networkName)["address"] 47 | client = make_client( 48 | networkName, node_uri, Erc20Client, contract_address=token_address, **clientArgs 49 | ) 50 | return cast(Erc20Client, client) 51 | -------------------------------------------------------------------------------- /src/web3client/helpers/debug.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | from typing import Any, Union 3 | 4 | from eth_typing.encoding import HexStr 5 | from web3.datastructures import AttributeDict 6 | from web3.types import TxReceipt 7 | 8 | from web3client.base_client import BaseClient 9 | 10 | 11 | def printTxInfo(client: BaseClient, txHash: HexStr) -> None: 12 | """ 13 | Get a transaction receipt and print it, together with 14 | the tx cost 15 | """ 16 | print(">>> TX SENT!") 17 | print("Hash = " + txHash) 18 | print("Waiting for transaction to finalize...") 19 | tx_receipt = client.get_tx_receipt(txHash) 20 | print(">>> TX IS ON THE BLOCKCHAIN :-)") 21 | pprint.pprint(tx_receipt) 22 | print(">>> ETH SPENT") 23 | print(BaseClient.get_gas_spent_in_eth(tx_receipt)) 24 | 25 | 26 | def pprintAttributeDict( 27 | attributeDict: Union[TxReceipt, AttributeDict[str, Any]] 28 | ) -> None: 29 | """ 30 | Web3 often returns AttributeDict instead of simple Dictionaries; 31 | this function pretty prints an AttributeDict 32 | """ 33 | print(formatAttributeDict(attributeDict)) 34 | 35 | 36 | def formatAttributeDict( 37 | attributeDict: Union[TxReceipt, AttributeDict[str, Any]], 38 | indent: int = 4, 39 | nestLevel: int = 0, 40 | ) -> str: 41 | """ 42 | Web3 often returns AttributeDict instead of simple Dictionaries; 43 | this function return a pretty string with the AttributeDict content 44 | """ 45 | prefix = nestLevel * indent * " " 46 | output = prefix + "{\n" 47 | for key, value in attributeDict.items(): 48 | if isinstance(value, AttributeDict): 49 | output += prefix + formatAttributeDict(value, indent, nestLevel + 1) 50 | output += "\n" 51 | else: 52 | output += prefix + " " * indent 53 | output += f"{key} -> {pprint.pformat(value, indent=indent)}" 54 | output += "\n" 55 | output += prefix + "}" 56 | 57 | return output 58 | -------------------------------------------------------------------------------- /tests/web3client/test_base_client_poa.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from web3.exceptions import ExtraDataLengthError 3 | from web3.middleware import geth_poa_middleware 4 | 5 | from web3client.base_client import BaseClient 6 | 7 | POA_CHAIN_RPC = "https://polygon-rpc.com" 8 | NON_POA_CHAIN_RPC = "https://arb1.arbitrum.io/rpc" 9 | 10 | 11 | @pytest.mark.remote 12 | def test_base_client_poa_with_poa_chain() -> None: 13 | client = BaseClient(node_uri=POA_CHAIN_RPC) 14 | block = client.get_latest_block() 15 | assert type(block.get("number")) is int 16 | assert block.get("number") >= 0 17 | 18 | 19 | @pytest.mark.remote 20 | def test_base_client_poa_with_poa_chain_if_no_support_raises() -> None: 21 | client = BaseClient(node_uri=POA_CHAIN_RPC, add_poa_support=False) 22 | with pytest.raises(ExtraDataLengthError): 23 | client.get_latest_block() 24 | 25 | 26 | @pytest.mark.remote 27 | def test_base_client_poa_with_normal_chain() -> None: 28 | client = BaseClient(node_uri=NON_POA_CHAIN_RPC) 29 | block = client.get_latest_block() 30 | assert type(block.get("number")) is int 31 | assert block.get("number") >= 0 32 | 33 | 34 | @pytest.mark.remote 35 | def test_base_client_poa_middleware() -> None: 36 | # Should work with middleware only. This works only if the middleware 37 | # is added at layer 0, so if the test fails it is likely you added 38 | # the middleware at the wrong layer (or using .add instead of .inject) 39 | client = BaseClient( 40 | node_uri=POA_CHAIN_RPC, middlewares=[geth_poa_middleware], add_poa_support=False 41 | ) 42 | block = client.get_latest_block() 43 | assert type(block.get("number")) is int 44 | assert block.get("number") >= 0 45 | 46 | 47 | @pytest.mark.remote 48 | def test_base_client_poa_middleware_and_no_poa_support() -> None: 49 | # Should work with middleware and poa support 50 | client = BaseClient(node_uri=POA_CHAIN_RPC, middlewares=[geth_poa_middleware]) 51 | block = client.get_latest_block() 52 | assert type(block.get("number")) is int 53 | assert block.get("number") >= 0 54 | -------------------------------------------------------------------------------- /tests/web3client/test_sign.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from web3client.base_client import BaseClient 4 | 5 | signers = [ 6 | { 7 | "name": "vanity_1", 8 | "address": "0x0c2010dc4736bab060740D3968cf1dDF86196D81", 9 | "private_key": "d94e4166f0b3c85ffebed3e0eaa7f7680ae296cf8a7229d637472b7452c8602c", 10 | }, 11 | { 12 | "name": "vanity_2", 13 | "address": "0x206D4d644c22dDFc343b3AD23bBc7A42c8B201fc", 14 | "private_key": "3bc2f9b05ac28389fd65fd40068a10f730ec66b6293f9cfd8fe804d212ce06bb", 15 | }, 16 | { 17 | "name": "vanity_3", 18 | "address": "0x9fF0c40eDe4585a5E9f0F00009ce79b6344cB663", 19 | "private_key": "f76c67c2dd62222a5ec747116a66c573f3795c53276c0cdeafbcb5f597e2f8d4", 20 | }, 21 | ] 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "msg", 26 | [ 27 | "Hello world!", 28 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam pulvinar lacus erat, et sollicitudin purus rutrum sed. Aliquam pulvinar nunc nec sagittis sagittis. Nunc efficitur lacus urna, sed dapibus lacus varius id. Nam laoreet convallis nisl, ut lacinia sem congue eu. Phasellus eu nisi in lectus lobortis viverra a at diam. Nulla dolor nisl, mollis efficitur venenatis in, elementum consequat quam. Sed a euismod justo, quis maximus velit. Maecenas varius augue dolor, sit amet elementum lacus pretium vitae. Fusce egestas condimentum quam eget elementum. Suspendisse vulputate ut urna a pretium. Nunc semper a sem fermentum dapibus.", 29 | "I will copiously donate to coccoinomane", 30 | ], 31 | ) 32 | def test_sign(msg: str) -> None: 33 | for signer in signers: 34 | client = BaseClient(node_uri=None, private_key=signer["private_key"]) 35 | signed_message = client.sign_message(msg) 36 | assert client.is_message_signed_by_me(msg, signed_message) 37 | assert "messageHash" in signed_message._asdict() 38 | assert "r" in signed_message._asdict() 39 | assert "s" in signed_message._asdict() 40 | assert "v" in signed_message._asdict() 41 | assert "signature" in signed_message._asdict() 42 | -------------------------------------------------------------------------------- /src/web3factory/erc20_tokens.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from web3client.exceptions import ( 4 | Erc20TokenNotFound, 5 | Erc20TokenNotUnique, 6 | NetworkNotFound, 7 | ) 8 | from web3factory.networks import is_network_supported 9 | from web3factory.types import Erc20TokenConfig 10 | 11 | """ 12 | List of supported ERC20 tokens acrosso networks 13 | """ 14 | supported_tokens: List[Erc20TokenConfig] = [ 15 | # Ethereum 16 | { 17 | "name": "USDC", 18 | "network": "eth", 19 | "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 20 | "decimals": 6, 21 | }, 22 | # Binance 23 | { 24 | "name": "BUSD", 25 | "network": "bnb", 26 | "address": "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", 27 | "decimals": 18, 28 | }, 29 | { 30 | "name": "BETH", 31 | "network": "bnb", 32 | "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", 33 | "decimals": 18, 34 | }, 35 | # Avalanche 36 | { 37 | "name": "USDC", 38 | "network": "avax", 39 | "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", 40 | "decimals": 6, 41 | }, 42 | ] 43 | 44 | 45 | def get_token_config(name: str, network: str) -> Erc20TokenConfig: 46 | """ 47 | Return the configuration for the given token on the given 48 | network; raises an exception if not found or more than one 49 | is found 50 | """ 51 | # Raise if network does not exist 52 | if not is_network_supported(network): 53 | raise NetworkNotFound(f"Network '{network}' not supported") 54 | # Get all tokens with given name and network 55 | tokens: List[Erc20TokenConfig] = [ 56 | t for t in supported_tokens if t["name"] == name and t["network"] == network 57 | ] 58 | # Must have exactly one token 59 | if len(tokens) == 0: 60 | raise Erc20TokenNotFound(f"ERC20 token '{name}' on '{network}' not supported") 61 | if len(tokens) > 1: 62 | raise Erc20TokenNotUnique( 63 | f"Found more than one ERC20 token with '{name}' on '{network}' (found '{len(tokens)}')" 64 | ) 65 | # Return the one token 66 | return tokens[0] 67 | 68 | 69 | def is_token_supported(name: str, network: str) -> bool: 70 | """ 71 | Return true if the given token and network pair is 72 | supported by the client factory 73 | """ 74 | try: 75 | get_token_config(name, network) 76 | return True 77 | except Erc20TokenNotFound: 78 | return False 79 | -------------------------------------------------------------------------------- /src/web3client/helpers/subscribe.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Callable, Dict, List, Tuple, Union 3 | 4 | from websockets.client import WebSocketClientProtocol 5 | 6 | from web3client.exceptions import Web3ClientException 7 | from web3client.types import SubscriptionType 8 | 9 | 10 | async def subscribe_to_notification( 11 | ws: WebSocketClientProtocol, 12 | type: SubscriptionType, 13 | on_subscribe: Callable[[Any, SubscriptionType], None] = None, 14 | logs_addresses: List[str] = None, 15 | logs_topics: List[str] = None, 16 | ) -> str: 17 | """Given a websocket connection to an RPC, subscribe to the given 18 | notification type, and return the subscription id. 19 | 20 | Optinally, call the given callback with the subscription response.""" 21 | # Build subsciption params 22 | params: Dict[str, Any] = { 23 | "jsonrpc": "2.0", 24 | "id": 1, 25 | "method": "eth_subscribe", 26 | "params": [type], 27 | } 28 | # Add logs filter if needed 29 | if type == "logs": 30 | logs_args: Dict[str, Any] = {} 31 | if logs_addresses: 32 | logs_args["address"] = logs_addresses 33 | if logs_topics: 34 | logs_args["topics"] = logs_topics 35 | params["params"].append(logs_args) 36 | # Subscribe to the notification type 37 | await ws.send(json.dumps(params)) 38 | subscription_response = await ws.recv() 39 | try: 40 | subscription_id = json.loads(subscription_response)["result"] 41 | except Exception as e: 42 | raise Web3ClientException(f"Failed to subscribe to {type}: {e}") 43 | # Call on_subscribe callback 44 | if on_subscribe: 45 | on_subscribe(json.loads(subscription_response), type) 46 | return subscription_id 47 | 48 | 49 | def parse_notification( 50 | notification: Union[str, bytes], type: SubscriptionType 51 | ) -> Tuple[str, Any]: 52 | """Given a notification, return the subscription ID and 53 | notification data""" 54 | try: 55 | as_dict = json.loads(notification) 56 | except json.JSONDecodeError as e: 57 | raise Web3ClientException( 58 | f"Notification from websocket is malformed: {notification!r}" 59 | ) 60 | 61 | try: 62 | subscription_id = as_dict["params"]["subscription"] 63 | except KeyError: 64 | raise Web3ClientException( 65 | f"Cannot extract 'subscription' field from websocket notification: {notification!r}" 66 | ) 67 | 68 | try: 69 | data = as_dict["params"]["result"] 70 | except KeyError: 71 | raise Web3ClientException( 72 | f"Cannot extract 'result' field from websocket notification: {notification!r}" 73 | ) 74 | 75 | return subscription_id, data 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Batteries-included client to interact with blockchains and smart contracts; used by [`web3cli`](https://github.com/coccoinomane/web3cli) and [crabada.py](https://github.com/coccoinomane/crabada.py). 2 | 3 | # Features 4 | 5 | - Easily create a client to interact with EVM-compatible chains 6 | - Works with Ethereum, Binance, Avalanche, Arbitrum One, zkSync Era, etc. 7 | - Subscribe to pending transactions in the mempool and new blocks 8 | - Flexible logging of RPC calls and transactions 9 | - Interact with tokens and ETH with the same dual interface 10 | - Includes a client for Compound V2 operations, and its clones 11 | - Save gas by setting an upper limit on the base fee 12 | - Need more flexibility? Use directly the underlying web3.py client 13 | 14 | 15 | # Install 16 | 17 | ```bash 18 | pip3 install -U web3client 19 | ``` 20 | 21 | # Examples 22 | 23 | - Stream pending transactions on the zkSync Era network: 24 | 25 | ```python 26 | from web3client.base_client import BaseClient 27 | 28 | client = BaseClient("wss://mainnet.era.zksync.io/ws") 29 | client.subscribe(lambda tx, _, __: print(f"Pending tx: {tx}")) 30 | ``` 31 | 32 | - Send 1 ETH and 100 USDC to Unicef, using a dual client: 33 | ```python 34 | from web3client.erc20_client import DualClient 35 | 36 | rpc = "https://cloudflare-eth.com" 37 | private_key = "0x..." 38 | unicef = "0xA59B29d7dbC9794d1e7f45123C48b2b8d0a34636" 39 | USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 40 | 41 | usdc_client = DualClient(rpc, private_key=private_key, contract_address=USDC) 42 | usdc_client.send_eth(unicef, 1) 43 | usdc_client.transfer(unicef, 100) 44 | ``` 45 | 46 | # More examples 47 | 48 | Please find more examples 49 | 50 | - in the [examples folder](./examples), and 51 | - in the [tests folder](./tests). 52 | 53 | 54 | # Test suite `web3test` 55 | 56 | `web3client` comes with several pytest plugins you can use to test your scripts: 57 | 58 | - `web3test-ape`: fixtures of accounts and smart contracts (erc20, compound, etc) 59 | - `web3test-web3client`: fixtures of clients for various smart contracts 60 | - `web3test-web3factory`: fixtures of clients for various chains 61 | 62 | To use one or more plugins in your script, add the following lines at the top of your `conftest.py``: 63 | 64 | ```python 65 | pytest_plugins = [ 66 | "web3test-ape", "web3test-web3client", "web3test-web3factory" 67 | ] 68 | ``` 69 | 70 | The order of the plugins in the aray is important. 71 | 72 | # It doesn't work 😡 73 | 74 | Don't panic! Instead... 75 | 76 | 1. Please check if your issue is listed in the [Issues tab](https://github.com/coccoinomane/web3client/issues). 77 | 2. If not, consider [writing a new issue](https://github.com/coccoinomane/web3client/issues/new) 🙂 78 | 79 | # Contributing 80 | 81 | All contributions are welcome! To start improving `web3client`, please refer to our [__contribution guide__](./CONTRIBUTING.md). 82 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/EIP20Interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | /** 5 | * @title ERC 20 Token Standard Interface 6 | * https://eips.ethereum.org/EIPS/eip-20 7 | */ 8 | interface EIP20Interface { 9 | function name() external view returns (string memory); 10 | function symbol() external view returns (string memory); 11 | function decimals() external view returns (uint8); 12 | 13 | /** 14 | * @notice Get the total number of tokens in circulation 15 | * @return The supply of tokens 16 | */ 17 | function totalSupply() external view returns (uint256); 18 | 19 | /** 20 | * @notice Gets the balance of the specified address 21 | * @param owner The address from which the balance will be retrieved 22 | * @return balance The balance 23 | */ 24 | function balanceOf(address owner) external view returns (uint256 balance); 25 | 26 | /** 27 | * @notice Transfer `amount` tokens from `msg.sender` to `dst` 28 | * @param dst The address of the destination account 29 | * @param amount The number of tokens to transfer 30 | * @return success Whether or not the transfer succeeded 31 | */ 32 | function transfer(address dst, uint256 amount) external returns (bool success); 33 | 34 | /** 35 | * @notice Transfer `amount` tokens from `src` to `dst` 36 | * @param src The address of the source account 37 | * @param dst The address of the destination account 38 | * @param amount The number of tokens to transfer 39 | * @return success Whether or not the transfer succeeded 40 | */ 41 | function transferFrom(address src, address dst, uint256 amount) external returns (bool success); 42 | 43 | /** 44 | * @notice Approve `spender` to transfer up to `amount` from `src` 45 | * @dev This will overwrite the approval amount for `spender` 46 | * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) 47 | * @param spender The address of the account which may transfer tokens 48 | * @param amount The number of tokens that are approved (-1 means infinite) 49 | * @return success Whether or not the approval succeeded 50 | */ 51 | function approve(address spender, uint256 amount) external returns (bool success); 52 | 53 | /** 54 | * @notice Get the current allowance from `owner` for `spender` 55 | * @param owner The address of the account which owns the tokens to be spent 56 | * @param spender The address of the account which may transfer tokens 57 | * @return remaining The number of tokens allowed to be spent (-1 means infinite) 58 | */ 59 | function allowance(address owner, address spender) external view returns (uint256 remaining); 60 | 61 | event Transfer(address indexed from, address indexed to, uint256 amount); 62 | event Approval(address indexed owner, address indexed spender, uint256 amount); 63 | } 64 | -------------------------------------------------------------------------------- /src/web3client/helpers/general.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from typing import Any, List 3 | from collections import Counter 4 | from random import uniform 5 | 6 | 7 | def firstOrNone(list: List[Any]) -> Any: 8 | """ 9 | Return the first element of a list or None if it is not set 10 | """ 11 | return nthOrNone(list, 0) 12 | 13 | 14 | def secondOrNone(list: List[Any]) -> Any: 15 | """ 16 | Return the second element of a list or None if it is not set 17 | """ 18 | return nthOrNone(list, 1) 19 | 20 | 21 | def thirdOrNone(list: List[Any]) -> Any: 22 | """ 23 | Return the third element of a list or None if it is not set 24 | """ 25 | return nthOrNone(list, 2) 26 | 27 | 28 | def fourthOrNone(list: List[Any]) -> Any: 29 | """ 30 | Return the fourth element of a list or None if it is not set 31 | """ 32 | return nthOrNone(list, 3) 33 | 34 | 35 | def nthOrNone(list: List[Any], n: int) -> Any: 36 | """ 37 | Return the n-th plus 1 element of a list or None if it is not set 38 | """ 39 | try: 40 | return list[n] 41 | except: 42 | return None 43 | 44 | 45 | def nthOrLastOrNone(list: List[Any], n: int) -> Any: 46 | """ 47 | Return the n-th element of a list; if it is not set, return 48 | the last element of the list; if it is not set, return none. 49 | """ 50 | if not list: 51 | return None 52 | return list[n] if len(list) > n else list[-1] 53 | 54 | 55 | def findInListOfDicts(l: List[dict[str, Any]], key: str, value: Any) -> Any: 56 | """ 57 | Return the first dictionary in the list that has the 58 | given key value 59 | """ 60 | return firstOrNone([item for item in l if item[key] == value]) 61 | 62 | 63 | def indexInList(l: List[Any], value: Any, doPop: bool = False) -> int: 64 | """ 65 | Wrapper to the list.index(value) method which returns None 66 | if the value is not found, instead of raising an exception. 67 | """ 68 | try: 69 | i = l.index(value) 70 | if doPop: 71 | l.pop(i) 72 | return i 73 | except ValueError: 74 | return None 75 | 76 | 77 | def duplicatesInList(l: List[Any]) -> List[Any]: 78 | """ 79 | Return duplicate elements in the given list 80 | 81 | Source: https://stackoverflow.com/a/9835819/2972183 82 | """ 83 | return [item for item, count in Counter(l).items() if count > 1] 84 | 85 | 86 | def flattenList(l: List[Any]) -> List[Any]: 87 | """ 88 | Flatten a list 89 | 90 | Source: https://stackoverflow.com/a/46080186/2972183 91 | """ 92 | return reduce(lambda x, y: x + y, l) 93 | 94 | 95 | def randomize(x: float, d: float) -> float: 96 | """ 97 | Return a number uniformly distributed between 98 | x*(1-d) and x*(1+d) 99 | """ 100 | if d == 0: 101 | return x 102 | return x * (1 + uniform(-1, 1) * d) 103 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "web3client" 3 | version = "1.3.11" 4 | description = "Batteries-included client to interact with blockchains and smart contracts" 5 | authors = [ 6 | {name = "coccoinomane", email = "coccoinomane@gmail.com"}, 7 | ] 8 | readme = "README.md" 9 | keywords = ["web3", "blockchain", "ethereum", "evm"] 10 | license = {text = "MIT"} 11 | requires-python = ">=3.9" 12 | dependencies = [ 13 | "web3>=6.0.0", 14 | "eth-typing>=2.3.0", 15 | "setuptools>=67.2.0", 16 | "typing-extensions>=4.4.0", 17 | "pre-commit>=2.21.0", 18 | "websockets>=10.0", 19 | "py-evm>=0.7.0a4", 20 | ] 21 | 22 | [tool.pdm.dev-dependencies] 23 | dev = [ 24 | "mypy>=1.5.1", 25 | "eth-ape[recommended-plugins]>=0.6.0", 26 | "isort>=5.12.0", 27 | "pre-commit>=3.3.3", 28 | "autoflake>=2.2.0", 29 | "black>=23.3.0", 30 | "pytest>=7.0.0", 31 | ] 32 | 33 | [tool.pdm.scripts] 34 | test = "ape test tests --network ::foundry" 35 | publish_test = "pdm publish -r testpypi -u ${PDM_PUBLISH_TEST_USERNAME} -P ${PDM_PUBLISH_TEST_PASSWORD}" 36 | release = "gh release create v{args} dist/web3client-{args}.tar.gz dist/web3client-{args}-py3-none-any.whl --generate-notes" 37 | mypy = "mypy src tests" 38 | 39 | [project.entry-points.pytest11] 40 | web3test-ape = "web3test.ape.fixtures" 41 | web3test-web3client = "web3test.web3client.fixtures" 42 | web3test-web3factory = "web3test.web3factory.fixtures" 43 | 44 | [project.urls] 45 | homepage = "https://github.com/coccoinomane/web3client" 46 | repository = "https://github.com/coccoinomane/web3client" 47 | 48 | [build-system] 49 | requires = ["pdm-pep517>=1.0.0"] 50 | build-backend = "pdm.pep517.api" 51 | 52 | [tool.black] 53 | line-length = 88 54 | 55 | [tool.setuptools.package-data] 56 | "web3client" = ["py.typed"] 57 | "web3factory" = ["py.typed"] 58 | "web3test" = ["py.typed"] 59 | 60 | [tool.mypy] 61 | check_untyped_defs = true 62 | disallow_any_generics = true 63 | disallow_incomplete_defs = true 64 | disallow_untyped_defs = true 65 | ignore_missing_imports = false 66 | no_implicit_optional = true 67 | show_error_codes = true 68 | strict_equality = true 69 | strict_optional = false 70 | warn_redundant_casts = true 71 | warn_unused_configs = true 72 | warn_unused_ignores = true 73 | disallow_any_unimported = false 74 | disallow_untyped_calls = true 75 | exclude = [ 76 | '__pypackages__', 77 | ] 78 | 79 | [tool.pytest.ini_options] 80 | filterwarnings = "ignore::DeprecationWarning" 81 | markers = [ 82 | "remote: tests that need an internet connection (deselect with '-m \"not remote\"')", 83 | "local: tests that require a local blockchain to be run, e.g. ganache, anvil or hardhat network (deselect with '-m \"not local\"')", 84 | "contracts: tests of the ape contracts", 85 | ] 86 | 87 | [tool.isort] 88 | profile = "black" 89 | src_paths = ["src", "tests"] 90 | 91 | [tool.autoflake] 92 | in_place = true 93 | remove_all_unused_imports = true 94 | ignore_pass_after_docstring = true 95 | remove_unused_variables = false 96 | ignore_init_module_imports = true -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/ComptrollerInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | abstract contract ComptrollerInterface { 5 | /// @notice Indicator that this is a Comptroller contract (for inspection) 6 | bool public constant isComptroller = true; 7 | 8 | /*** Assets You Are In ***/ 9 | 10 | function enterMarkets(address[] calldata cTokens) virtual external returns (uint[] memory); 11 | function exitMarket(address cToken) virtual external returns (uint); 12 | 13 | /*** Policy Hooks ***/ 14 | 15 | function mintAllowed(address cToken, address minter, uint mintAmount) virtual external returns (uint); 16 | function mintVerify(address cToken, address minter, uint mintAmount, uint mintTokens) virtual external; 17 | 18 | function redeemAllowed(address cToken, address redeemer, uint redeemTokens) virtual external returns (uint); 19 | function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) virtual external; 20 | 21 | function borrowAllowed(address cToken, address borrower, uint borrowAmount) virtual external returns (uint); 22 | function borrowVerify(address cToken, address borrower, uint borrowAmount) virtual external; 23 | 24 | function repayBorrowAllowed( 25 | address cToken, 26 | address payer, 27 | address borrower, 28 | uint repayAmount) virtual external returns (uint); 29 | function repayBorrowVerify( 30 | address cToken, 31 | address payer, 32 | address borrower, 33 | uint repayAmount, 34 | uint borrowerIndex) virtual external; 35 | 36 | function liquidateBorrowAllowed( 37 | address cTokenBorrowed, 38 | address cTokenCollateral, 39 | address liquidator, 40 | address borrower, 41 | uint repayAmount) virtual external returns (uint); 42 | function liquidateBorrowVerify( 43 | address cTokenBorrowed, 44 | address cTokenCollateral, 45 | address liquidator, 46 | address borrower, 47 | uint repayAmount, 48 | uint seizeTokens) virtual external; 49 | 50 | function seizeAllowed( 51 | address cTokenCollateral, 52 | address cTokenBorrowed, 53 | address liquidator, 54 | address borrower, 55 | uint seizeTokens) virtual external returns (uint); 56 | function seizeVerify( 57 | address cTokenCollateral, 58 | address cTokenBorrowed, 59 | address liquidator, 60 | address borrower, 61 | uint seizeTokens) virtual external; 62 | 63 | function transferAllowed(address cToken, address src, address dst, uint transferTokens) virtual external returns (uint); 64 | function transferVerify(address cToken, address src, address dst, uint transferTokens) virtual external; 65 | 66 | /*** Liquidity/Liquidation Calculations ***/ 67 | 68 | function liquidateCalculateSeizeTokens( 69 | address cTokenBorrowed, 70 | address cTokenCollateral, 71 | uint repayAmount) virtual external view returns (uint, uint); 72 | } 73 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/EIP20NonStandardInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | /** 5 | * @title EIP20NonStandardInterface 6 | * @dev Version of ERC20 with no return values for `transfer` and `transferFrom` 7 | * See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca 8 | */ 9 | interface EIP20NonStandardInterface { 10 | 11 | /** 12 | * @notice Get the total number of tokens in circulation 13 | * @return The supply of tokens 14 | */ 15 | function totalSupply() external view returns (uint256); 16 | 17 | /** 18 | * @notice Gets the balance of the specified address 19 | * @param owner The address from which the balance will be retrieved 20 | * @return balance The balance 21 | */ 22 | function balanceOf(address owner) external view returns (uint256 balance); 23 | 24 | /// 25 | /// !!!!!!!!!!!!!! 26 | /// !!! NOTICE !!! `transfer` does not return a value, in violation of the ERC-20 specification 27 | /// !!!!!!!!!!!!!! 28 | /// 29 | 30 | /** 31 | * @notice Transfer `amount` tokens from `msg.sender` to `dst` 32 | * @param dst The address of the destination account 33 | * @param amount The number of tokens to transfer 34 | */ 35 | function transfer(address dst, uint256 amount) external; 36 | 37 | /// 38 | /// !!!!!!!!!!!!!! 39 | /// !!! NOTICE !!! `transferFrom` does not return a value, in violation of the ERC-20 specification 40 | /// !!!!!!!!!!!!!! 41 | /// 42 | 43 | /** 44 | * @notice Transfer `amount` tokens from `src` to `dst` 45 | * @param src The address of the source account 46 | * @param dst The address of the destination account 47 | * @param amount The number of tokens to transfer 48 | */ 49 | function transferFrom(address src, address dst, uint256 amount) external; 50 | 51 | /** 52 | * @notice Approve `spender` to transfer up to `amount` from `src` 53 | * @dev This will overwrite the approval amount for `spender` 54 | * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) 55 | * @param spender The address of the account which may transfer tokens 56 | * @param amount The number of tokens that are approved 57 | * @return success Whether or not the approval succeeded 58 | */ 59 | function approve(address spender, uint256 amount) external returns (bool success); 60 | 61 | /** 62 | * @notice Get the current allowance from `owner` for `spender` 63 | * @param owner The address of the account which owns the tokens to be spent 64 | * @param spender The address of the account which may transfer tokens 65 | * @return remaining The number of tokens allowed to be spent 66 | */ 67 | function allowance(address owner, address spender) external view returns (uint256 remaining); 68 | 69 | event Transfer(address indexed from, address indexed to, uint256 amount); 70 | event Approval(address indexed owner, address indexed spender, uint256 amount); 71 | } 72 | -------------------------------------------------------------------------------- /tests/web3client/test_base_client.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.base_client import BaseClient 7 | from web3client.middlewares.rpc_log_middleware import MemoryLog 8 | 9 | 10 | @pytest.mark.local 11 | def test_base_client_balance_in_wei( 12 | base_client: BaseClient, 13 | alice: ape.api.AccountAPI, 14 | ) -> None: 15 | alice_balance_in_wei = base_client.get_balance_in_wei(alice.address) 16 | assert alice.balance == alice_balance_in_wei 17 | 18 | 19 | @pytest.mark.local 20 | def test_base_client_transfer( 21 | alice_base_client: BaseClient, 22 | bob: ape.api.AccountAPI, 23 | ) -> None: 24 | bob_balance = bob.balance 25 | alice_base_client.send_eth_in_wei(bob.address, 10**18) 26 | assert bob.balance == bob_balance + 10**18 27 | 28 | 29 | @pytest.mark.local 30 | def test_base_client_transfer_non_checksum_address( 31 | alice_base_client: BaseClient, 32 | bob: ape.api.AccountAPI, 33 | ) -> None: 34 | bob_balance = bob.balance 35 | alice_base_client.send_eth_in_wei(str(bob.address).lower(), 10**18) 36 | assert bob.balance == bob_balance + 10**18 37 | 38 | 39 | def test_base_client_clone(alice_base_client: BaseClient) -> None: 40 | alice_base_client_clone = alice_base_client.clone() 41 | assert alice_base_client_clone.node_uri == alice_base_client.node_uri 42 | assert alice_base_client_clone.private_key == alice_base_client.private_key 43 | 44 | 45 | @pytest.mark.local 46 | def test_base_client_rpc_logs( 47 | accounts_keys: List[str], ape_chain_uri: str, bob: ape.api.AccountAPI 48 | ) -> None: 49 | rpc_log = MemoryLog(rpc_whitelist=["eth_sendRawTransaction"]) 50 | client = BaseClient( 51 | node_uri=ape_chain_uri, private_key=accounts_keys[0], rpc_logs=[rpc_log] 52 | ) 53 | tx_hash = client.send_eth_in_wei(bob.address.lower(), 10**18) 54 | assert len(rpc_log.entries) == 2 55 | assert len(rpc_log.get_requests()) == 1 56 | assert len(rpc_log.get_responses()) == 1 57 | request = rpc_log.get_requests()[0] 58 | response = rpc_log.get_responses()[0] 59 | assert request.method == "eth_sendRawTransaction" 60 | assert response.method == "eth_sendRawTransaction" 61 | assert response.response["result"] == tx_hash 62 | 63 | 64 | @pytest.mark.local 65 | def test_base_client_rpc_logs_class_defined( 66 | accounts_keys: List[str], ape_chain_uri: str, bob: ape.api.AccountAPI 67 | ) -> None: 68 | rpc_log = MemoryLog(rpc_whitelist=["eth_sendRawTransaction"]) 69 | 70 | class BaseClientWithClassDefinedRpcLog(BaseClient): 71 | rpc_logs = [rpc_log] 72 | 73 | client = BaseClientWithClassDefinedRpcLog( 74 | node_uri=ape_chain_uri, private_key=accounts_keys[0] 75 | ) 76 | tx_hash = client.send_eth_in_wei(bob.address.lower(), 10**18) 77 | assert len(rpc_log.entries) == 2 78 | assert len(rpc_log.get_requests()) == 1 79 | assert len(rpc_log.get_responses()) == 1 80 | request = rpc_log.get_requests()[0] 81 | response = rpc_log.get_responses()[0] 82 | assert request.method == "eth_sendRawTransaction" 83 | assert response.method == "eth_sendRawTransaction" 84 | assert response.response["result"] == tx_hash 85 | -------------------------------------------------------------------------------- /tests/web3client/test_dual_client.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.erc20_client import DualClient 7 | from web3client.exceptions import Web3ClientException 8 | 9 | # ___ ___ __ 10 | # | __| _ _ __ |_ ) / \ 11 | # | _| | '_| / _| / / | () | 12 | # |___| |_| \__| /___| \__/ 13 | 14 | 15 | @pytest.mark.local 16 | def test_dual_client_token_balance_in_wei( 17 | TST: ape.contracts.ContractInstance, 18 | dual_client_token: DualClient, 19 | alice: ape.api.AccountAPI, 20 | ) -> None: 21 | alice_balance_in_wei = dual_client_token.balance_in_wei(alice.address) 22 | assert TST.balanceOf(alice) == alice_balance_in_wei 23 | 24 | 25 | @pytest.mark.local 26 | def test_dual_client_token_transfer( 27 | TST: ape.contracts.ContractInstance, 28 | alice_dual_client_token: DualClient, 29 | bob: ape.api.AccountAPI, 30 | ) -> None: 31 | bob_balance = TST.balanceOf(bob) 32 | alice_dual_client_token.transfer(bob.address, 10**18) 33 | assert TST.balanceOf(bob) == bob_balance + 10**18 34 | 35 | 36 | def test_dual_client_token_clone( 37 | alice_dual_client_token: DualClient, 38 | TST_0: ape.contracts.ContractInstance, 39 | ) -> None: 40 | clone = cast(DualClient, alice_dual_client_token.clone(base=DualClient)) 41 | # The clone's contract must be the same as the original's 42 | assert clone.contract_address == alice_dual_client_token.contract_address 43 | assert clone.abi == alice_dual_client_token.abi 44 | # Setting a property on the clone must not change the original 45 | old_contract_address = alice_dual_client_token.contract_address 46 | clone.set_contract(TST_0.address) 47 | assert alice_dual_client_token.contract_address == old_contract_address 48 | assert clone.contract_address == TST_0.address 49 | 50 | 51 | # _ _ _ _ 52 | # | \| | __ _ | |_ (_) __ __ ___ 53 | # | .` | / _` | | _| | | \ V / / -_) 54 | # |_|\_| \__,_| \__| |_| \_/ \___| 55 | 56 | 57 | @pytest.mark.local 58 | def test_dual_token_native_balance_in_wei( 59 | dual_client_native: DualClient, 60 | alice: ape.api.AccountAPI, 61 | ) -> None: 62 | alice_balance_in_wei = dual_client_native.balance_in_wei(alice.address) 63 | assert alice.balance == alice_balance_in_wei 64 | 65 | 66 | @pytest.mark.local 67 | def test_dual_token_native_transfer( 68 | alice_dual_client_native: DualClient, 69 | bob: ape.api.AccountAPI, 70 | ) -> None: 71 | bob_balance = bob.balance 72 | alice_dual_client_native.transfer(bob.address, 10**18) 73 | assert bob.balance == bob_balance + 10**18 74 | 75 | 76 | def test_dual_token_native_clone(alice_dual_client_native: DualClient) -> None: 77 | clone = alice_dual_client_native.clone() 78 | assert clone.node_uri == alice_dual_client_native.node_uri 79 | assert clone.private_key == alice_dual_client_native.private_key 80 | 81 | 82 | def test_dual_token_native_exceptions( 83 | alice_dual_client_native: DualClient, 84 | bob: ape.api.AccountAPI, 85 | ) -> None: 86 | with pytest.raises(Web3ClientException, match="value_in_wei"): 87 | alice_dual_client_native.transfer(bob.address, 10**18, value_in_wei=10**18) 88 | with pytest.raises(Web3ClientException, match="total supply"): 89 | alice_dual_client_native.total_supply() 90 | with pytest.raises(Web3ClientException, match="approve"): 91 | alice_dual_client_native.approve(bob.address, 10**18) 92 | -------------------------------------------------------------------------------- /src/web3test/web3client/fixtures/compound_v2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | import ape 6 | from web3client.compound_v2_client import ( 7 | CompoundV2CErc20Client, 8 | CompoundV2CEtherClient, 9 | CompoundV2ComptrollerClient, 10 | ) 11 | 12 | 13 | @pytest.fixture() 14 | def compound_v2_ceth_client( 15 | compound_v2_ceth: ape.contracts.ContractInstance, ape_chain_uri: str 16 | ) -> CompoundV2CEtherClient: 17 | return CompoundV2CEtherClient( 18 | node_uri=ape_chain_uri, contract_address=compound_v2_ceth.address 19 | ) 20 | 21 | 22 | @pytest.fixture() 23 | def alice_compound_v2_ceth_client( 24 | compound_v2_ceth: ape.contracts.ContractInstance, 25 | accounts_keys: List[str], 26 | ape_chain_uri: str, 27 | ) -> CompoundV2CEtherClient: 28 | return CompoundV2CEtherClient( 29 | node_uri=ape_chain_uri, 30 | contract_address=compound_v2_ceth.address, 31 | private_key=accounts_keys[0], 32 | ) 33 | 34 | 35 | @pytest.fixture() 36 | def bob_compound_v2_ceth_client( 37 | compound_v2_ceth: ape.contracts.ContractInstance, 38 | accounts_keys: List[str], 39 | ape_chain_uri: str, 40 | ) -> CompoundV2CEtherClient: 41 | return CompoundV2CEtherClient( 42 | node_uri=ape_chain_uri, 43 | contract_address=compound_v2_ceth.address, 44 | private_key=accounts_keys[1], 45 | ) 46 | 47 | 48 | # CErc20 client (cTST) 49 | 50 | 51 | @pytest.fixture() 52 | def compound_v2_ctst_client( 53 | compound_v2_ctst: ape.contracts.ContractInstance, ape_chain_uri: str 54 | ) -> CompoundV2CErc20Client: 55 | return CompoundV2CErc20Client( 56 | node_uri=ape_chain_uri, contract_address=compound_v2_ctst.address 57 | ) 58 | 59 | 60 | @pytest.fixture() 61 | def alice_compound_v2_ctst_client( 62 | compound_v2_ctst: ape.contracts.ContractInstance, 63 | accounts_keys: List[str], 64 | ape_chain_uri: str, 65 | ) -> CompoundV2CErc20Client: 66 | return CompoundV2CErc20Client( 67 | node_uri=ape_chain_uri, 68 | contract_address=compound_v2_ctst.address, 69 | private_key=accounts_keys[0], 70 | ) 71 | 72 | 73 | @pytest.fixture() 74 | def bob_compound_v2_ctst_client( 75 | compound_v2_ctst: ape.contracts.ContractInstance, 76 | accounts_keys: List[str], 77 | ape_chain_uri: str, 78 | ) -> CompoundV2CErc20Client: 79 | return CompoundV2CErc20Client( 80 | node_uri=ape_chain_uri, 81 | contract_address=compound_v2_ctst.address, 82 | private_key=accounts_keys[1], 83 | ) 84 | 85 | 86 | # Comptroller client 87 | 88 | 89 | @pytest.fixture() 90 | def compound_v2_comptroller_client( 91 | compound_v2_comptroller: ape.contracts.ContractInstance, ape_chain_uri: str 92 | ) -> CompoundV2ComptrollerClient: 93 | return CompoundV2ComptrollerClient( 94 | node_uri=ape_chain_uri, contract_address=compound_v2_comptroller.address 95 | ) 96 | 97 | 98 | @pytest.fixture() 99 | def alice_compound_v2_comptroller_client( 100 | compound_v2_comptroller: ape.contracts.ContractInstance, 101 | accounts_keys: List[str], 102 | ape_chain_uri: str, 103 | ) -> CompoundV2ComptrollerClient: 104 | return CompoundV2ComptrollerClient( 105 | node_uri=ape_chain_uri, 106 | contract_address=compound_v2_comptroller.address, 107 | private_key=accounts_keys[0], 108 | ) 109 | 110 | 111 | @pytest.fixture() 112 | def bob_compound_v2_comptroller_client( 113 | compound_v2_comptroller: ape.contracts.ContractInstance, 114 | accounts_keys: List[str], 115 | ape_chain_uri: str, 116 | ) -> CompoundV2ComptrollerClient: 117 | return CompoundV2ComptrollerClient( 118 | node_uri=ape_chain_uri, 119 | contract_address=compound_v2_comptroller.address, 120 | private_key=accounts_keys[1], 121 | ) 122 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/WhitePaperInterestRateModel.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./InterestRateModel.sol"; 5 | 6 | /** 7 | * @title Compound's WhitePaperInterestRateModel Contract 8 | * @author Compound 9 | * @notice The parameterized model described in section 2.4 of the original Compound Protocol whitepaper 10 | */ 11 | contract WhitePaperInterestRateModel is InterestRateModel { 12 | event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock); 13 | 14 | uint256 private constant BASE = 1e18; 15 | 16 | /** 17 | * @notice The approximate number of blocks per year that is assumed by the interest rate model 18 | */ 19 | uint public constant blocksPerYear = 2102400; 20 | 21 | /** 22 | * @notice The multiplier of utilization rate that gives the slope of the interest rate 23 | */ 24 | uint public multiplierPerBlock; 25 | 26 | /** 27 | * @notice The base interest rate which is the y-intercept when utilization rate is 0 28 | */ 29 | uint public baseRatePerBlock; 30 | 31 | /** 32 | * @notice Construct an interest rate model 33 | * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE) 34 | * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by BASE) 35 | */ 36 | constructor(uint baseRatePerYear, uint multiplierPerYear) public { 37 | baseRatePerBlock = baseRatePerYear / blocksPerYear; 38 | multiplierPerBlock = multiplierPerYear / blocksPerYear; 39 | 40 | emit NewInterestParams(baseRatePerBlock, multiplierPerBlock); 41 | } 42 | 43 | /** 44 | * @notice Calculates the utilization rate of the market: `borrows / (cash + borrows - reserves)` 45 | * @param cash The amount of cash in the market 46 | * @param borrows The amount of borrows in the market 47 | * @param reserves The amount of reserves in the market (currently unused) 48 | * @return The utilization rate as a mantissa between [0, BASE] 49 | */ 50 | function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) { 51 | // Utilization rate is 0 when there are no borrows 52 | if (borrows == 0) { 53 | return 0; 54 | } 55 | 56 | return borrows * BASE / (cash + borrows - reserves); 57 | } 58 | 59 | /** 60 | * @notice Calculates the current borrow rate per block, with the error code expected by the market 61 | * @param cash The amount of cash in the market 62 | * @param borrows The amount of borrows in the market 63 | * @param reserves The amount of reserves in the market 64 | * @return The borrow rate percentage per block as a mantissa (scaled by BASE) 65 | */ 66 | function getBorrowRate(uint cash, uint borrows, uint reserves) override public view returns (uint) { 67 | uint ur = utilizationRate(cash, borrows, reserves); 68 | return (ur * multiplierPerBlock / BASE) + baseRatePerBlock; 69 | } 70 | 71 | /** 72 | * @notice Calculates the current supply rate per block 73 | * @param cash The amount of cash in the market 74 | * @param borrows The amount of borrows in the market 75 | * @param reserves The amount of reserves in the market 76 | * @param reserveFactorMantissa The current reserve factor for the market 77 | * @return The supply rate percentage per block as a mantissa (scaled by BASE) 78 | */ 79 | function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) { 80 | uint oneMinusReserveFactor = BASE - reserveFactorMantissa; 81 | uint borrowRate = getBorrowRate(cash, borrows, reserves); 82 | uint rateToPool = borrowRate * oneMinusReserveFactor / BASE; 83 | return utilizationRate(cash, borrows, reserves) * rateToPool / BASE; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/web3test/ape/helpers/compound_v2.py: -------------------------------------------------------------------------------- 1 | import ape 2 | 3 | 4 | def deploy_comptroller( 5 | accounts: ape.managers.accounts.AccountManager, 6 | comptroller_container: ape.contracts.ContractContainer, 7 | price_oracle_instance: ape.contracts.ContractInstance, 8 | ) -> ape.contracts.ContractInstance: 9 | """Deploy the Compound comptroller (https://docs.compound.finance/v2/comptroller/). 10 | On Ethereum: 11 | - implementation: https://etherscan.io/address/0xBafE01ff935C7305907c33BF824352eE5979B526#code 12 | - proxy: https://etherscan.io/address/0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B#code 13 | To call Comptroller functions, use the Comptroller ABI on the Unitroller address. 14 | """ 15 | comptroller = comptroller_container.deploy(sender=accounts[0]) 16 | comptroller._setPriceOracle(price_oracle_instance, sender=accounts[0]) 17 | return comptroller 18 | 19 | 20 | def deploy_interest_rate_model( 21 | accounts: ape.managers.accounts.AccountManager, 22 | interest_rate_model_container: ape.contracts.ContractContainer, 23 | base_rate: int, 24 | multiplier: int, 25 | ) -> ape.contracts.ContractInstance: 26 | """Deploy the Compound interest rate model. On Ethereum: 27 | https://etherscan.io/address/0xc64C4cBA055eFA614CE01F4BAD8A9F519C4f8FaB#code 28 | """ 29 | return interest_rate_model_container.deploy( 30 | base_rate, multiplier, sender=accounts[0] 31 | ) 32 | 33 | 34 | def deploy_cerc20( 35 | accounts: ape.managers.accounts.AccountManager, 36 | cerc20_container: ape.contracts.ContractContainer, 37 | underlying_token_instance: ape.contracts.ContractInstance, 38 | comptroller_instance: ape.contracts.ContractInstance, 39 | interest_rate_model_instance: ape.contracts.ContractInstance, 40 | initial_exchange_rate_mantissa: int = 200000000000000, # 0.02 * 10 ** (6 - 8) * 10**18 41 | name: str = "CompoundTestToken", 42 | symbol: str = "cTST", 43 | decimals: int = 8, 44 | collateral_factor: int = 9 * 10**17, 45 | ) -> ape.contracts.ContractInstance: 46 | """Deploy a Compound pool with an underlying test token. 47 | See constructor arguments here: 48 | https://etherscan.io/address/0x39AA39c021dfbaE8faC545936693aC917d5E7563#code 49 | """ 50 | cerc20 = cerc20_container.deploy( 51 | underlying_token_instance, 52 | comptroller_instance, 53 | interest_rate_model_instance, 54 | initial_exchange_rate_mantissa, 55 | name, 56 | symbol, 57 | decimals, 58 | accounts[0], 59 | sender=accounts[0], 60 | ) 61 | # List cTST on the comptroller 62 | comptroller_instance._supportMarket(cerc20, sender=accounts[0]) 63 | # Set collateral factor 64 | comptroller_instance._setCollateralFactor( 65 | cerc20, collateral_factor, sender=accounts[0] 66 | ) 67 | return cerc20 68 | 69 | 70 | def deploy_cether( 71 | accounts: ape.managers.accounts.AccountManager, 72 | ceth_container: ape.contracts.ContractContainer, 73 | comptroller_instance: ape.contracts.ContractInstance, 74 | interest_rate_model_instance: ape.contracts.ContractInstance, 75 | initial_exchange_rate_mantissa: int = 200000000000000000000000000, # 0.02 * 10 ** (18 - 8) * 10**18 76 | name: str = "CompoundETH", 77 | symbol: str = "cETH", 78 | decimals: int = 8, 79 | collateral_factor: int = 9 * 10**17, 80 | ) -> ape.contracts.ContractInstance: 81 | """Deploy a Compound pool with ETH. The difference is that 82 | there is no underlying token. See constructor arguments here: 83 | https://etherscan.io/address/0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5#code 84 | """ 85 | ceth = ceth_container.deploy( 86 | comptroller_instance, 87 | interest_rate_model_instance, 88 | initial_exchange_rate_mantissa, 89 | name, 90 | symbol, 91 | decimals, 92 | accounts[0], 93 | sender=accounts[0], 94 | ) 95 | # List cETH as a supported market 96 | comptroller_instance._supportMarket(ceth, sender=accounts[0]) 97 | # Set collateral factor 98 | comptroller_instance._setCollateralFactor( 99 | ceth, collateral_factor, sender=accounts[0] 100 | ) 101 | return ceth 102 | -------------------------------------------------------------------------------- /src/web3client/helpers/tx.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, cast 2 | 3 | from eth.abc import SignedTransactionAPI 4 | from eth.vm.forks.arrow_glacier.transactions import ( 5 | ArrowGlacierTransactionBuilder as TransactionBuilder, 6 | ) 7 | from eth_utils import encode_hex, to_bytes 8 | from hexbytes import HexBytes 9 | from web3 import Web3 10 | from web3.types import AccessList, Nonce, RPCResponse, TxData, Wei 11 | 12 | 13 | def parse_raw_tx_pyevm(raw_tx: str) -> SignedTransactionAPI: 14 | """Convert a raw transaction to a py-evm signed transaction object. 15 | 16 | Inspired by: 17 | - https://github.com/ethereum/web3.py/issues/3109#issuecomment-1737744506 18 | - https://snakecharmers.ethereum.org/web3-py-patterns-decoding-signed-transactions/ 19 | """ 20 | return TransactionBuilder().decode(to_bytes(hexstr=raw_tx)) 21 | 22 | 23 | def parse_raw_tx(raw_tx: str) -> TxData: 24 | """Convert a raw transaction to a web3.py TxData dict. 25 | 26 | Inspired by: 27 | - https://ethereum.stackexchange.com/a/83855/89782 28 | - https://docs.ethers.org/v5/api/utils/transactions/#utils-parseTransaction 29 | """ 30 | tx = parse_raw_tx_pyevm(raw_tx) 31 | 32 | return { 33 | "accessList": cast(AccessList, tx.access_list), 34 | "blockHash": None, 35 | "blockNumber": None, 36 | "chainId": tx.chain_id, 37 | "data": HexBytes(Web3.to_hex(tx.data)), 38 | "from": Web3.to_checksum_address(encode_hex(tx.sender)), 39 | "gas": tx.gas, 40 | "gasPrice": None if tx.type_id is not None else cast(Wei, tx.gas_price), 41 | "maxFeePerGas": cast(Wei, tx.max_fee_per_gas), 42 | "maxPriorityFeePerGas": cast(Wei, tx.max_priority_fee_per_gas), 43 | "hash": HexBytes(tx.hash), 44 | "input": None, 45 | "nonce": cast(Nonce, tx.nonce), 46 | "r": HexBytes(tx.r), 47 | "s": HexBytes(tx.s), 48 | "to": Web3.to_checksum_address(tx.to), 49 | "transactionIndex": None, 50 | "type": tx.type_id, 51 | "v": None, 52 | "value": cast(Wei, tx.value), 53 | } 54 | 55 | 56 | def parse_estimate_gas_tx(params: Dict[str, Any]) -> TxData: 57 | """Takes the parameters passed to the RPC method eth_estimateGas 58 | and returns a TxData dict with them""" 59 | 60 | maxFeePerGas = ( 61 | cast(Wei, int(params["maxFeePerGas"], 16)) 62 | if params.get("maxFeePerGas") 63 | else None 64 | ) 65 | maxPriorityFeePerGas = ( 66 | cast(Wei, int(params["maxPriorityFeePerGas"], 16)) 67 | if params.get("maxPriorityFeePerGas") 68 | else None 69 | ) 70 | gasPrice = cast( 71 | Wei, int(params["gasPrice"], 16) if params.get("gasPrice") else None 72 | ) 73 | 74 | return { 75 | "accessList": params.get("accessList", ()), 76 | "blockHash": None, 77 | "blockNumber": None, 78 | "chainId": int(params["chainId"], 16) if params.get("chainId") else None, 79 | "data": HexBytes(params["data"]) if params.get("data") else None, 80 | "from": params.get("from", None), 81 | "gas": params.get("gas", None), 82 | "gasPrice": gasPrice, 83 | "maxFeePerGas": maxFeePerGas, 84 | "maxPriorityFeePerGas": maxPriorityFeePerGas, 85 | "hash": None, 86 | "input": None, 87 | "nonce": cast(Nonce, int(params["nonce"], 16)) if params.get("nonce") else None, 88 | "r": None, 89 | "s": None, 90 | "to": params.get("to", None), 91 | "transactionIndex": None, 92 | "type": int(params["type"], 16) if params.get("type") else None, 93 | "v": None, 94 | "value": cast(Wei, int(params["value"], 16) if params.get("value") else 0), 95 | } 96 | 97 | 98 | def parse_call_tx(params: Dict[str, Any]) -> TxData: 99 | """Takes the parameters passed to the RPC method eth_call 100 | and returns a TxData dict with them. This is treated exactly the same as an 101 | eth_estimateGas call.""" 102 | return parse_estimate_gas_tx(params) 103 | 104 | 105 | def is_rpc_response_ok(response: RPCResponse) -> bool: 106 | """Check if an RPC response did not error""" 107 | return ( 108 | "error" not in response 109 | and "result" in response 110 | and response["result"] is not None 111 | ) 112 | -------------------------------------------------------------------------------- /src/web3test/ape/fixtures/compound_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyTest Fixtures. 3 | """ 4 | 5 | import pytest 6 | 7 | import ape 8 | from web3test.ape.helpers.compound_v2 import ( 9 | deploy_cerc20, 10 | deploy_cether, 11 | deploy_comptroller, 12 | deploy_interest_rate_model, 13 | ) 14 | 15 | 16 | @pytest.fixture(scope="function") 17 | def CompoundV2Comptroller() -> ape.contracts.ContractContainer: 18 | """Use this to deploy a Compound V2 comptroller contract.""" 19 | return ape.project.get_contract("Comptroller") 20 | 21 | 22 | @pytest.fixture(scope="function") 23 | def CompoundV2InterestRateModel() -> ape.contracts.ContractContainer: 24 | """Use this to deploy the Compound V2 white paper interest rate model contract.""" 25 | return ape.project.get_contract("WhitePaperInterestRateModel") 26 | 27 | 28 | @pytest.fixture(scope="function") 29 | def CompoundV2FixedPriceOracle() -> ape.contracts.ContractContainer: 30 | """Use this to deploy a test fixed price oracle contract for Compound V2.""" 31 | return ape.project.get_contract("FixedPriceOracle") 32 | 33 | 34 | @pytest.fixture(scope="function") 35 | def CompoundV2Erc20() -> ape.contracts.ContractContainer: 36 | """Use this to deploy a Compound V2 ERC20 market contract.""" 37 | return ape.project.get_contract("CErc20Immutable") 38 | 39 | 40 | @pytest.fixture(scope="function") 41 | def CompoundV2Ether() -> ape.contracts.ContractContainer: 42 | """Use this to deploy a Compound V2 Ether market contract.""" 43 | return ape.project.get_contract("CEther") 44 | 45 | 46 | @pytest.fixture(scope="function") 47 | def compound_v2_price_oracle( 48 | accounts: ape.managers.accounts.AccountManager, 49 | CompoundV2FixedPriceOracle: ape.contracts.ContractContainer, 50 | ) -> ape.contracts.ContractInstance: 51 | """The Compound V2 price oracle contract. This is a simple 52 | implementation that sets the price of each token to 1 ETH.""" 53 | return CompoundV2FixedPriceOracle.deploy(10**18, sender=accounts[0]) 54 | 55 | 56 | @pytest.fixture(scope="function") 57 | def compound_v2_comptroller( 58 | accounts: ape.managers.accounts.AccountManager, 59 | CompoundV2Comptroller: ape.contracts.ContractContainer, 60 | compound_v2_price_oracle: ape.contracts.ContractInstance, 61 | ) -> ape.contracts.ContractInstance: 62 | """The Compound V2 comptroller contract.""" 63 | return deploy_comptroller(accounts, CompoundV2Comptroller, compound_v2_price_oracle) 64 | 65 | 66 | @pytest.fixture(scope="function") 67 | def compound_v2_interest_rate_model( 68 | accounts: ape.managers.accounts.AccountManager, 69 | CompoundV2InterestRateModel: ape.contracts.ContractContainer, 70 | ) -> ape.contracts.ContractInstance: 71 | """The Compound V2 interest rate model contract. Parameters taken from 72 | the Ethereum contract (0xc64C4cBA055eFA614CE01F4BAD8A9F519C4f8FaB)""" 73 | return deploy_interest_rate_model( 74 | accounts, 75 | CompoundV2InterestRateModel, 76 | base_rate=0, 77 | multiplier=int(0.2 * 10**18), 78 | ) 79 | 80 | 81 | @pytest.fixture(scope="function") 82 | def compound_v2_ceth( 83 | accounts: ape.managers.accounts.AccountManager, 84 | CompoundV2Ether: ape.contracts.ContractContainer, 85 | compound_v2_comptroller: ape.contracts.ContractInstance, 86 | compound_v2_interest_rate_model: ape.contracts.ContractInstance, 87 | ) -> ape.contracts.ContractInstance: 88 | """The contract for the Compound V2 cETH money market""" 89 | return deploy_cether( 90 | accounts, 91 | CompoundV2Ether, 92 | compound_v2_comptroller, 93 | compound_v2_interest_rate_model, 94 | ) 95 | 96 | 97 | @pytest.fixture(scope="function") 98 | def compound_v2_ctst( 99 | accounts: ape.managers.accounts.AccountManager, 100 | CompoundV2Erc20: ape.contracts.ContractContainer, 101 | compound_v2_comptroller: ape.contracts.ContractInstance, 102 | compound_v2_interest_rate_model: ape.contracts.ContractInstance, 103 | TST: ape.contracts.ContractInstance, 104 | ) -> ape.contracts.ContractInstance: 105 | """The contract for the Compound V2 cTST money market, where 106 | TST is the underlying asset""" 107 | return deploy_cerc20( 108 | accounts, 109 | CompoundV2Erc20, 110 | TST, 111 | compound_v2_comptroller, 112 | compound_v2_interest_rate_model, 113 | ) 114 | -------------------------------------------------------------------------------- /src/web3factory/networks.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from typing import Any, List, cast 3 | 4 | from web3.middleware import geth_poa_middleware 5 | 6 | from web3client.exceptions import NetworkNotFound 7 | from web3client.helpers.general import findInListOfDicts 8 | from web3factory.types import NetworkConfig 9 | 10 | """ 11 | List of supported networks (aka blockchains), each with its own 12 | parameters. 13 | """ 14 | supported_networks: List[NetworkConfig] = [ 15 | # Ethereum 16 | { 17 | "name": "eth", 18 | "tx_type": 2, 19 | "chain_id": 1, 20 | "rpcs": [ 21 | "https://cloudflare-eth.com", 22 | ], 23 | "coin": "ETH", 24 | }, 25 | # Avalanche C Chain 26 | { 27 | "name": "bnb", 28 | "tx_type": 0, 29 | "chain_id": 56, 30 | "middlewares": [geth_poa_middleware], 31 | "rpcs": [ 32 | "https://bsc-dataseed.binance.org/", 33 | ], 34 | "coin": "BNB", 35 | }, 36 | # Avalanche C Chain 37 | { 38 | "name": "avax", 39 | "tx_type": 2, 40 | "chain_id": 43114, 41 | "middlewares": [geth_poa_middleware], 42 | "rpcs": [ 43 | "https://api.avax.network/ext/bc/C/rpc", 44 | ], 45 | "coin": "AVAX", 46 | }, 47 | # Polygon 48 | { 49 | "name": "poly", 50 | "tx_type": 2, 51 | "chain_id": 137, 52 | "middlewares": [geth_poa_middleware], 53 | "rpcs": ["https://polygon-rpc.com"], 54 | "coin": "MATIC", 55 | }, 56 | # Arbitrum One 57 | { 58 | "name": "arb", 59 | "tx_type": 2, 60 | "chain_id": 42161, 61 | "rpcs": ["https://arb1.arbitrum.io/rpc"], 62 | "coin": "ETH", 63 | }, 64 | # zkSync Era 65 | { 66 | "name": "era", 67 | "tx_type": 2, 68 | "chain_id": 324, 69 | "rpcs": ["https://mainnet.era.zksync.io"], 70 | "coin": "ETH", 71 | }, 72 | # Kava EVM 73 | { 74 | "name": "kava", 75 | "tx_type": 0, 76 | "chain_id": 2222, 77 | "rpcs": ["https://evm.kava.io"], 78 | "coin": "KAVA", 79 | }, 80 | # Optimism 81 | { 82 | "name": "opt", 83 | "tx_type": 0, 84 | "chain_id": 10, 85 | "rpcs": ["https://mainnet.optimism.io"], 86 | "coin": "ETH", 87 | }, 88 | # Scroll 89 | { 90 | "name": "scroll", 91 | "tx_type": 0, 92 | "chain_id": 534352, 93 | "middlewares": [geth_poa_middleware], 94 | "rpcs": ["https://mainnet-rpc.scroll.io"], 95 | "coin": "ETH", 96 | }, 97 | # ZKFair 98 | { 99 | "name": "zkf", 100 | "tx_type": 0, 101 | "chain_id": 42766, 102 | "rpcs": ["https://rpc.zkfair.io"], 103 | "coin": "USDC", 104 | }, 105 | # Manta Pacific 106 | { 107 | "name": "manta", 108 | "tx_type": 0, 109 | "chain_id": 169, 110 | "rpcs": ["https://pacific-rpc.manta.network/http"], 111 | "coin": "ETH", 112 | }, 113 | ] 114 | 115 | 116 | def get_network_config(name: str) -> NetworkConfig: 117 | """ 118 | Return the configuration for the network with the given 119 | name; raises an exception if not found 120 | """ 121 | network: NetworkConfig = findInListOfDicts( 122 | cast(Any, supported_networks), "name", name 123 | ) 124 | if network is None: 125 | raise NetworkNotFound(f"Network '{name}' not supported") 126 | return network 127 | 128 | 129 | def pick_random_rpc(network_name: str) -> str: 130 | """ 131 | Given a network return one of its RPCs, randomly, 132 | or None, if it has no RPC 133 | """ 134 | network = get_network_config(network_name) 135 | rpcs = network.get("rpcs") 136 | return choice(rpcs) if rpcs else None 137 | 138 | 139 | def pick_first_rpc(network_name: str) -> str: 140 | """ 141 | Given a network return its first RPC or None, 142 | if it has no RPC 143 | """ 144 | network = get_network_config(network_name) 145 | rpcs = network.get("rpcs") 146 | return rpcs[0] if rpcs else None 147 | 148 | 149 | def is_network_supported(name: str) -> bool: 150 | """ 151 | Return true if the given network is supported by 152 | the client factory 153 | """ 154 | try: 155 | get_network_config(name) 156 | return True 157 | except NetworkNotFound: 158 | return False 159 | -------------------------------------------------------------------------------- /tests/ape/contracts/token/Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | // SPDX-License-Identifier: MIT 4 | 5 | import "./Token_SafeMath.sol"; 6 | 7 | /** 8 | @title Bare-bones Token implementation 9 | @notice Based on the ERC-20 token standard as defined at 10 | https://eips.ethereum.org/EIPS/eip-20 11 | */ 12 | contract Token { 13 | 14 | using Token_SafeMath for uint256; 15 | 16 | string public symbol; 17 | string public name; 18 | uint256 public decimals; 19 | uint256 public totalSupply; 20 | 21 | mapping(address => uint256) balances; 22 | mapping(address => mapping(address => uint256)) allowed; 23 | 24 | event Transfer(address indexed from, address indexed to, uint256 value); 25 | event Approval(address indexed owner, address indexed spender, uint256 value); 26 | 27 | constructor( 28 | string memory _name, 29 | string memory _symbol, 30 | uint256 _decimals, 31 | uint256 _totalSupply 32 | ) 33 | public 34 | { 35 | name = _name; 36 | symbol = _symbol; 37 | decimals = _decimals; 38 | totalSupply = _totalSupply; 39 | balances[msg.sender] = _totalSupply; 40 | emit Transfer(address(0), msg.sender, _totalSupply); 41 | } 42 | 43 | /** 44 | @notice Getter to check the current balance of an address 45 | @param _owner Address to query the balance of 46 | @return Token balance 47 | */ 48 | function balanceOf(address _owner) public view returns (uint256) { 49 | return balances[_owner]; 50 | } 51 | 52 | /** 53 | @notice Getter to check the amount of tokens that an owner allowed to a spender 54 | @param _owner The address which owns the funds 55 | @param _spender The address which will spend the funds 56 | @return The amount of tokens still available for the spender 57 | */ 58 | function allowance( 59 | address _owner, 60 | address _spender 61 | ) 62 | public 63 | view 64 | returns (uint256) 65 | { 66 | return allowed[_owner][_spender]; 67 | } 68 | 69 | /** 70 | @notice Approve an address to spend the specified amount of tokens on behalf of msg.sender 71 | @dev Beware that changing an allowance with this method brings the risk that someone may use both the old 72 | and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 73 | race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 74 | https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 75 | @param _spender The address which will spend the funds. 76 | @param _value The amount of tokens to be spent. 77 | @return Success boolean 78 | */ 79 | function approve(address _spender, uint256 _value) public returns (bool) { 80 | allowed[msg.sender][_spender] = _value; 81 | emit Approval(msg.sender, _spender, _value); 82 | return true; 83 | } 84 | 85 | /** shared logic for transfer and transferFrom */ 86 | function _transfer(address _from, address _to, uint256 _value) internal { 87 | require(balances[_from] >= _value, "Insufficient balance"); 88 | balances[_from] = balances[_from].sub(_value); 89 | balances[_to] = balances[_to].add(_value); 90 | emit Transfer(_from, _to, _value); 91 | } 92 | 93 | /** 94 | @notice Transfer tokens to a specified address 95 | @param _to The address to transfer to 96 | @param _value The amount to be transferred 97 | @return Success boolean 98 | */ 99 | function transfer(address _to, uint256 _value) public returns (bool) { 100 | _transfer(msg.sender, _to, _value); 101 | return true; 102 | } 103 | 104 | /** 105 | @notice Transfer tokens from one address to another 106 | @param _from The address which you want to send tokens from 107 | @param _to The address which you want to transfer to 108 | @param _value The amount of tokens to be transferred 109 | @return Success boolean 110 | */ 111 | function transferFrom( 112 | address _from, 113 | address _to, 114 | uint256 _value 115 | ) 116 | public 117 | returns (bool) 118 | { 119 | require(allowed[_from][msg.sender] >= _value, "Insufficient allowance"); 120 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); 121 | _transfer(_from, _to, _value); 122 | return true; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/ErrorReporter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | contract ComptrollerErrorReporter { 5 | enum Error { 6 | NO_ERROR, 7 | UNAUTHORIZED, 8 | COMPTROLLER_MISMATCH, 9 | INSUFFICIENT_SHORTFALL, 10 | INSUFFICIENT_LIQUIDITY, 11 | INVALID_CLOSE_FACTOR, 12 | INVALID_COLLATERAL_FACTOR, 13 | INVALID_LIQUIDATION_INCENTIVE, 14 | MARKET_NOT_ENTERED, // no longer possible 15 | MARKET_NOT_LISTED, 16 | MARKET_ALREADY_LISTED, 17 | MATH_ERROR, 18 | NONZERO_BORROW_BALANCE, 19 | PRICE_ERROR, 20 | REJECTION, 21 | SNAPSHOT_ERROR, 22 | TOO_MANY_ASSETS, 23 | TOO_MUCH_REPAY 24 | } 25 | 26 | enum FailureInfo { 27 | ACCEPT_ADMIN_PENDING_ADMIN_CHECK, 28 | ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK, 29 | EXIT_MARKET_BALANCE_OWED, 30 | EXIT_MARKET_REJECTION, 31 | SET_CLOSE_FACTOR_OWNER_CHECK, 32 | SET_CLOSE_FACTOR_VALIDATION, 33 | SET_COLLATERAL_FACTOR_OWNER_CHECK, 34 | SET_COLLATERAL_FACTOR_NO_EXISTS, 35 | SET_COLLATERAL_FACTOR_VALIDATION, 36 | SET_COLLATERAL_FACTOR_WITHOUT_PRICE, 37 | SET_IMPLEMENTATION_OWNER_CHECK, 38 | SET_LIQUIDATION_INCENTIVE_OWNER_CHECK, 39 | SET_LIQUIDATION_INCENTIVE_VALIDATION, 40 | SET_MAX_ASSETS_OWNER_CHECK, 41 | SET_PENDING_ADMIN_OWNER_CHECK, 42 | SET_PENDING_IMPLEMENTATION_OWNER_CHECK, 43 | SET_PRICE_ORACLE_OWNER_CHECK, 44 | SUPPORT_MARKET_EXISTS, 45 | SUPPORT_MARKET_OWNER_CHECK, 46 | SET_PAUSE_GUARDIAN_OWNER_CHECK 47 | } 48 | 49 | /** 50 | * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary 51 | * contract-specific code that enables us to report opaque error codes from upgradeable contracts. 52 | **/ 53 | event Failure(uint error, uint info, uint detail); 54 | 55 | /** 56 | * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator 57 | */ 58 | function fail(Error err, FailureInfo info) internal returns (uint) { 59 | emit Failure(uint(err), uint(info), 0); 60 | 61 | return uint(err); 62 | } 63 | 64 | /** 65 | * @dev use this when reporting an opaque error from an upgradeable collaborator contract 66 | */ 67 | function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) { 68 | emit Failure(uint(err), uint(info), opaqueError); 69 | 70 | return uint(err); 71 | } 72 | } 73 | 74 | contract TokenErrorReporter { 75 | uint public constant NO_ERROR = 0; // support legacy return codes 76 | 77 | error TransferComptrollerRejection(uint256 errorCode); 78 | error TransferNotAllowed(); 79 | error TransferNotEnough(); 80 | error TransferTooMuch(); 81 | 82 | error MintComptrollerRejection(uint256 errorCode); 83 | error MintFreshnessCheck(); 84 | 85 | error RedeemComptrollerRejection(uint256 errorCode); 86 | error RedeemFreshnessCheck(); 87 | error RedeemTransferOutNotPossible(); 88 | 89 | error BorrowComptrollerRejection(uint256 errorCode); 90 | error BorrowFreshnessCheck(); 91 | error BorrowCashNotAvailable(); 92 | 93 | error RepayBorrowComptrollerRejection(uint256 errorCode); 94 | error RepayBorrowFreshnessCheck(); 95 | 96 | error LiquidateComptrollerRejection(uint256 errorCode); 97 | error LiquidateFreshnessCheck(); 98 | error LiquidateCollateralFreshnessCheck(); 99 | error LiquidateAccrueBorrowInterestFailed(uint256 errorCode); 100 | error LiquidateAccrueCollateralInterestFailed(uint256 errorCode); 101 | error LiquidateLiquidatorIsBorrower(); 102 | error LiquidateCloseAmountIsZero(); 103 | error LiquidateCloseAmountIsUintMax(); 104 | error LiquidateRepayBorrowFreshFailed(uint256 errorCode); 105 | 106 | error LiquidateSeizeComptrollerRejection(uint256 errorCode); 107 | error LiquidateSeizeLiquidatorIsBorrower(); 108 | 109 | error AcceptAdminPendingAdminCheck(); 110 | 111 | error SetComptrollerOwnerCheck(); 112 | error SetPendingAdminOwnerCheck(); 113 | 114 | error SetReserveFactorAdminCheck(); 115 | error SetReserveFactorFreshCheck(); 116 | error SetReserveFactorBoundsCheck(); 117 | 118 | error AddReservesFactorFreshCheck(uint256 actualAddAmount); 119 | 120 | error ReduceReservesAdminCheck(); 121 | error ReduceReservesFreshCheck(); 122 | error ReduceReservesCashNotAvailable(); 123 | error ReduceReservesCashValidation(); 124 | 125 | error SetInterestRateModelOwnerCheck(); 126 | error SetInterestRateModelFreshCheck(); 127 | } 128 | -------------------------------------------------------------------------------- /src/web3test/ape/fixtures/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyTest Fixtures. 3 | """ 4 | 5 | from typing import Any, List, cast 6 | 7 | import pytest 8 | from web3.types import ABI 9 | 10 | import ape 11 | 12 | # ____ _ _ 13 | # / ___| | |__ __ _ (_) _ __ 14 | # | | | '_ \ / _` | | | | '_ \ 15 | # | |___ | | | | | (_| | | | | | | | 16 | # \____| |_| |_| \__,_| |_| |_| |_| 17 | 18 | 19 | @pytest.fixture(scope="session") 20 | def ape_chain( 21 | chain: ape.managers.chain.ChainManager, 22 | ) -> ape.managers.chain.ChainManager: 23 | """Alias for the 'chain' fixture of Brownie, to avoid naming 24 | conflicts with the Chain model of web3core.""" 25 | return chain 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def ape_chain_uri( 30 | chain: ape.managers.chain.ChainManager, 31 | ) -> str: 32 | """Return the URI of the local chain""" 33 | try: 34 | return cast(Any, chain.provider).uri 35 | except AttributeError: 36 | try: 37 | return cast(Any, chain.provider).web3.provider.endpoint_uri 38 | except AttributeError: 39 | return "http://localhost:8545" 40 | 41 | 42 | @pytest.fixture(scope="session") 43 | def is_eip1559( 44 | chain: ape.managers.chain.ChainManager, 45 | ) -> bool: 46 | """Return True if the local chain supports eip1599 (type 2 transactions).""" 47 | return hasattr(chain.blocks[0], "base_fee") or hasattr( 48 | chain.blocks[0], "base_fee_per_gas" 49 | ) 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def ape_chain_name( 54 | chain: ape.managers.chain.ChainManager, 55 | ) -> str: 56 | """Return whether we are running tests on ganache or anvil.""" 57 | if chain.chain_id == 1337: 58 | return "ganache" 59 | elif chain.chain_id == 31337: 60 | return "anvil" 61 | raise ValueError(f"Unknown chain type '{chain.chain_id}'") 62 | 63 | 64 | # _ _ 65 | # / \ ___ ___ ___ _ _ _ __ | |_ ___ 66 | # / _ \ / __| / __| / _ \ | | | | | '_ \ | __| / __| 67 | # / ___ \ | (__ | (__ | (_) | | |_| | | | | | | |_ \__ \ 68 | # /_/ \_\ \___| \___| \___/ \__,_| |_| |_| \__| |___/ 69 | 70 | 71 | @pytest.fixture(scope="session") 72 | def alice( 73 | accounts: ape.managers.accounts.AccountManager, accounts_keys: List[str] 74 | ) -> ape.api.AccountAPI: 75 | """A Brownie account preloaded in the local chain""" 76 | accounts[0].private_key = accounts_keys[0] 77 | return accounts[0] 78 | 79 | 80 | @pytest.fixture(scope="session") 81 | def bob( 82 | accounts: ape.managers.accounts.AccountManager, accounts_keys: List[str] 83 | ) -> ape.api.AccountAPI: 84 | """A Brownie account preloaded in the local chain""" 85 | accounts[1].private_key = accounts_keys[1] 86 | return accounts[1] 87 | 88 | 89 | @pytest.fixture(scope="session") 90 | def alice_private_key(accounts_keys: List[str]) -> str: 91 | return accounts_keys[0] 92 | 93 | 94 | @pytest.fixture(scope="session") 95 | def bob_private_key(accounts_keys: List[str]) -> str: 96 | return accounts_keys[1] 97 | 98 | 99 | @pytest.fixture(scope="session") 100 | def accounts_keys() -> List[str]: 101 | """Private keys of the local accounts created by ape. 102 | There are just the keys from the mnemonic phrase 103 | 'test test test test test test test test test test test junk' 104 | following the standard path m/44'/60'/0'/0/{account_index}""" 105 | return [ 106 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 107 | "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", 108 | "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", 109 | "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", 110 | "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", 111 | "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", 112 | "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", 113 | "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", 114 | "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", 115 | "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6", 116 | ] 117 | 118 | 119 | # _ ____ ___ 120 | # / \ | __ ) |_ _| 121 | # / _ \ | _ \ | | 122 | # / ___ \ | |_) | | | 123 | # /_/ \_\ |____/ |___| 124 | 125 | 126 | @pytest.fixture(scope="session") 127 | def simple_abi() -> ABI: 128 | """A simple ABI for a contract with a single function""" 129 | return [ 130 | { 131 | "constant": False, 132 | "inputs": [{"name": "a", "type": "uint256"}], 133 | "name": "foo", 134 | "outputs": [], 135 | "payable": False, 136 | "stateMutability": "nonpayable", 137 | "type": "function", 138 | } 139 | ] 140 | -------------------------------------------------------------------------------- /tests/web3client/test_parse_tx.py: -------------------------------------------------------------------------------- 1 | from hexbytes import HexBytes 2 | 3 | from web3client.helpers.tx import parse_raw_tx 4 | 5 | 6 | def test_parse_tx_type2_eth_transfer() -> None: 7 | # ETH transfer on Optimism (tx 0x8631361df65445a40fc46cff4625a2c070e618733d9ebdf31a31535276225b85) 8 | raw_tx = "0x02f86a0a75843b9aca0084412e386682520894240abf8acb28205b92d39181e2dab0b0d8ea6e5d6480c080a0a942241378fafc80670c6dde3c39ba5b1c4f992cd26fa263e7bbc253696c9035a008cf3399808cb511f0171be2ba766ea5c9d424a97dae3fb24685c440eeefc4fa" 9 | expected = { 10 | "accessList": (), 11 | "blockHash": None, 12 | "blockNumber": None, 13 | "chainId": 10, 14 | "data": HexBytes("0x"), 15 | "from": "0x240AbF8ACB28205B92D39181e2Dab0B0D8eA6e5D", 16 | "gas": 21000, 17 | "gasPrice": None, 18 | "maxFeePerGas": 1093548134, 19 | "maxPriorityFeePerGas": 1000000000, 20 | "hash": HexBytes( 21 | "0x8631361df65445a40fc46cff4625a2c070e618733d9ebdf31a31535276225b85" 22 | ), 23 | "input": None, 24 | "nonce": 117, 25 | "r": HexBytes( 26 | "0xa942241378fafc80670c6dde3c39ba5b1c4f992cd26fa263e7bbc253696c9035" 27 | ), 28 | "s": HexBytes( 29 | "0x08cf3399808cb511f0171be2ba766ea5c9d424a97dae3fb24685c440eeefc4fa" 30 | ), 31 | "to": "0x240AbF8ACB28205B92D39181e2Dab0B0D8eA6e5D", 32 | "transactionIndex": None, 33 | "type": 2, 34 | "v": None, 35 | "value": 100, 36 | } 37 | assert parse_raw_tx(raw_tx) == expected 38 | 39 | 40 | def test_parse_tx_type2_token_transfer() -> None: 41 | # USDC transfer (tx 0x444d05672fd04d99d417ce2105f34414758bcfb0579b686197cbf829458e3477) 42 | raw_tx = "0x02f8b00101847735940085060db8840082e4b794a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d6000000000000000000000000000000000000000000000000000000000042c1d80c001a02e18a3a75c10ba1be4ecb314e37c3a3eb5630be7e26692da3f3145f8b1efb54aa0458adf97080dc4955ea4e0f6d628cdcd278af74e9c65a2f15b7c20e0005d2fd9" 43 | expected = { 44 | "accessList": (), 45 | "blockHash": None, 46 | "blockNumber": None, 47 | "chainId": 1, 48 | "data": HexBytes( 49 | "0xa9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d6000000000000000000000000000000000000000000000000000000000042c1d80" 50 | ), 51 | "from": "0xF693EC528B3837eAd9B82D16bEc5d2fD4E430006", 52 | "gas": 58551, 53 | "gasPrice": None, 54 | "maxFeePerGas": 26000000000, 55 | "maxPriorityFeePerGas": 2000000000, 56 | "hash": HexBytes( 57 | "0x444d05672fd04d99d417ce2105f34414758bcfb0579b686197cbf829458e3477" 58 | ), 59 | "input": None, 60 | "nonce": 1, 61 | "r": HexBytes( 62 | "0x2e18a3a75c10ba1be4ecb314e37c3a3eb5630be7e26692da3f3145f8b1efb54a" 63 | ), 64 | "s": HexBytes( 65 | "0x458adf97080dc4955ea4e0f6d628cdcd278af74e9c65a2f15b7c20e0005d2fd9" 66 | ), 67 | "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 68 | "transactionIndex": None, 69 | "type": 2, 70 | "v": None, 71 | "value": 0, 72 | } 73 | assert parse_raw_tx(raw_tx) == expected 74 | 75 | 76 | def test_parse_tx_legacy_token_transfer() -> None: 77 | # Token transfer on Ethereum (tx 0xb808400bd5a1dd9c37960c515d2493c380b829c5a592e499ed0d5d9913a6a446) 78 | raw_tx = "0xf8a910850684ee180082e48694a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f9200000000000000000000000000000000000000000000000000000000010366401ba0e2a4093875682ac6a1da94cdcc0a783fe61a7273d98e1ebfe77ace9cab91a120a00f553e48f3496b7329a7c0008b3531dd29490c517ad28b0e6c1fba03b79a1dee" 79 | expected = { 80 | "accessList": [], 81 | "blockHash": None, 82 | "blockNumber": None, 83 | "chainId": None, 84 | "data": HexBytes( 85 | "0xa9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f920000000000000000000000000000000000000000000000000000000001036640" 86 | ), 87 | "from": "0xD8cE57B469962b6Ea944d28b741312Fb7E78cfaF", 88 | "gas": 58502, 89 | "gasPrice": 28000000000, 90 | "maxFeePerGas": 28000000000, 91 | "maxPriorityFeePerGas": 28000000000, 92 | "hash": HexBytes( 93 | "0xb808400bd5a1dd9c37960c515d2493c380b829c5a592e499ed0d5d9913a6a446" 94 | ), 95 | "input": None, 96 | "nonce": 16, 97 | "r": HexBytes( 98 | "0xe2a4093875682ac6a1da94cdcc0a783fe61a7273d98e1ebfe77ace9cab91a120" 99 | ), 100 | "s": HexBytes( 101 | "0x0f553e48f3496b7329a7c0008b3531dd29490c517ad28b0e6c1fba03b79a1dee" 102 | ), 103 | "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 104 | "transactionIndex": None, 105 | "type": None, 106 | "v": None, 107 | "value": 0, 108 | } 109 | assert parse_raw_tx(raw_tx) == expected 110 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/CEther.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./CToken.sol"; 5 | 6 | /** 7 | * @title Compound's CEther Contract 8 | * @notice CToken which wraps Ether 9 | * @author Compound 10 | */ 11 | contract CEther is CToken { 12 | /** 13 | * @notice Construct a new CEther money market 14 | * @param comptroller_ The address of the Comptroller 15 | * @param interestRateModel_ The address of the interest rate model 16 | * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 17 | * @param name_ ERC-20 name of this token 18 | * @param symbol_ ERC-20 symbol of this token 19 | * @param decimals_ ERC-20 decimal precision of this token 20 | * @param admin_ Address of the administrator of this token 21 | */ 22 | constructor(ComptrollerInterface comptroller_, 23 | InterestRateModel interestRateModel_, 24 | uint initialExchangeRateMantissa_, 25 | string memory name_, 26 | string memory symbol_, 27 | uint8 decimals_, 28 | address payable admin_) { 29 | // Creator of the contract is admin during initialization 30 | admin = payable(msg.sender); 31 | 32 | initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); 33 | 34 | // Set the proper admin now that initialization is done 35 | admin = admin_; 36 | } 37 | 38 | 39 | /*** User Interface ***/ 40 | 41 | /** 42 | * @notice Sender supplies assets into the market and receives cTokens in exchange 43 | * @dev Reverts upon any failure 44 | */ 45 | function mint() external payable { 46 | mintInternal(msg.value); 47 | } 48 | 49 | /** 50 | * @notice Sender redeems cTokens in exchange for the underlying asset 51 | * @dev Accrues interest whether or not the operation succeeds, unless reverted 52 | * @param redeemTokens The number of cTokens to redeem into underlying 53 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 54 | */ 55 | function redeem(uint redeemTokens) external returns (uint) { 56 | redeemInternal(redeemTokens); 57 | return NO_ERROR; 58 | } 59 | 60 | /** 61 | * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset 62 | * @dev Accrues interest whether or not the operation succeeds, unless reverted 63 | * @param redeemAmount The amount of underlying to redeem 64 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 65 | */ 66 | function redeemUnderlying(uint redeemAmount) external returns (uint) { 67 | redeemUnderlyingInternal(redeemAmount); 68 | return NO_ERROR; 69 | } 70 | 71 | /** 72 | * @notice Sender borrows assets from the protocol to their own address 73 | * @param borrowAmount The amount of the underlying asset to borrow 74 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 75 | */ 76 | function borrow(uint borrowAmount) external returns (uint) { 77 | borrowInternal(borrowAmount); 78 | return NO_ERROR; 79 | } 80 | 81 | /** 82 | * @notice Sender repays their own borrow 83 | * @dev Reverts upon any failure 84 | */ 85 | function repayBorrow() external payable { 86 | repayBorrowInternal(msg.value); 87 | } 88 | 89 | /** 90 | * @notice Sender repays a borrow belonging to borrower 91 | * @dev Reverts upon any failure 92 | * @param borrower the account with the debt being payed off 93 | */ 94 | function repayBorrowBehalf(address borrower) external payable { 95 | repayBorrowBehalfInternal(borrower, msg.value); 96 | } 97 | 98 | /** 99 | * @notice The sender liquidates the borrowers collateral. 100 | * The collateral seized is transferred to the liquidator. 101 | * @dev Reverts upon any failure 102 | * @param borrower The borrower of this cToken to be liquidated 103 | * @param cTokenCollateral The market in which to seize collateral from the borrower 104 | */ 105 | function liquidateBorrow(address borrower, CToken cTokenCollateral) external payable { 106 | liquidateBorrowInternal(borrower, msg.value, cTokenCollateral); 107 | } 108 | 109 | /** 110 | * @notice The sender adds to reserves. 111 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 112 | */ 113 | function _addReserves() external payable returns (uint) { 114 | return _addReservesInternal(msg.value); 115 | } 116 | 117 | /** 118 | * @notice Send Ether to CEther to mint 119 | */ 120 | receive() external payable { 121 | mintInternal(msg.value); 122 | } 123 | 124 | /*** Safe Token ***/ 125 | 126 | /** 127 | * @notice Gets balance of this contract in terms of Ether, before this message 128 | * @dev This excludes the value of the current message, if any 129 | * @return The quantity of Ether owned by this contract 130 | */ 131 | function getCashPrior() override internal view returns (uint) { 132 | return address(this).balance - msg.value; 133 | } 134 | 135 | /** 136 | * @notice Perform the actual transfer in, which is a no-op 137 | * @param from Address sending the Ether 138 | * @param amount Amount of Ether being sent 139 | * @return The actual amount of Ether transferred 140 | */ 141 | function doTransferIn(address from, uint amount) override internal returns (uint) { 142 | // Sanity checks 143 | require(msg.sender == from, "sender mismatch"); 144 | require(msg.value == amount, "value mismatch"); 145 | return amount; 146 | } 147 | 148 | function doTransferOut(address payable to, uint amount) virtual override internal { 149 | /* Send the Ether, with minimal gas and revert on failure */ 150 | to.transfer(amount); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/ComptrollerStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./CToken.sol"; 5 | import "./PriceOracle.sol"; 6 | 7 | contract UnitrollerAdminStorage { 8 | /** 9 | * @notice Administrator for this contract 10 | */ 11 | address public admin; 12 | 13 | /** 14 | * @notice Pending administrator for this contract 15 | */ 16 | address public pendingAdmin; 17 | 18 | /** 19 | * @notice Active brains of Unitroller 20 | */ 21 | address public comptrollerImplementation; 22 | 23 | /** 24 | * @notice Pending brains of Unitroller 25 | */ 26 | address public pendingComptrollerImplementation; 27 | } 28 | 29 | contract ComptrollerV1Storage is UnitrollerAdminStorage { 30 | 31 | /** 32 | * @notice Oracle which gives the price of any given asset 33 | */ 34 | PriceOracle public oracle; 35 | 36 | /** 37 | * @notice Multiplier used to calculate the maximum repayAmount when liquidating a borrow 38 | */ 39 | uint public closeFactorMantissa; 40 | 41 | /** 42 | * @notice Multiplier representing the discount on collateral that a liquidator receives 43 | */ 44 | uint public liquidationIncentiveMantissa; 45 | 46 | /** 47 | * @notice Max number of assets a single account can participate in (borrow or use as collateral) 48 | */ 49 | uint public maxAssets; 50 | 51 | /** 52 | * @notice Per-account mapping of "assets you are in", capped by maxAssets 53 | */ 54 | mapping(address => CToken[]) public accountAssets; 55 | 56 | } 57 | 58 | contract ComptrollerV2Storage is ComptrollerV1Storage { 59 | struct Market { 60 | // Whether or not this market is listed 61 | bool isListed; 62 | 63 | // Multiplier representing the most one can borrow against their collateral in this market. 64 | // For instance, 0.9 to allow borrowing 90% of collateral value. 65 | // Must be between 0 and 1, and stored as a mantissa. 66 | uint collateralFactorMantissa; 67 | 68 | // Per-market mapping of "accounts in this asset" 69 | mapping(address => bool) accountMembership; 70 | 71 | // Whether or not this market receives COMP 72 | bool isComped; 73 | } 74 | 75 | /** 76 | * @notice Official mapping of cTokens -> Market metadata 77 | * @dev Used e.g. to determine if a market is supported 78 | */ 79 | mapping(address => Market) public markets; 80 | 81 | 82 | /** 83 | * @notice The Pause Guardian can pause certain actions as a safety mechanism. 84 | * Actions which allow users to remove their own assets cannot be paused. 85 | * Liquidation / seizing / transfer can only be paused globally, not by market. 86 | */ 87 | address public pauseGuardian; 88 | bool public _mintGuardianPaused; 89 | bool public _borrowGuardianPaused; 90 | bool public transferGuardianPaused; 91 | bool public seizeGuardianPaused; 92 | mapping(address => bool) public mintGuardianPaused; 93 | mapping(address => bool) public borrowGuardianPaused; 94 | } 95 | 96 | contract ComptrollerV3Storage is ComptrollerV2Storage { 97 | struct CompMarketState { 98 | // The market's last updated compBorrowIndex or compSupplyIndex 99 | uint224 index; 100 | 101 | // The block number the index was last updated at 102 | uint32 block; 103 | } 104 | 105 | /// @notice A list of all markets 106 | CToken[] public allMarkets; 107 | 108 | /// @notice The rate at which the flywheel distributes COMP, per block 109 | uint public compRate; 110 | 111 | /// @notice The portion of compRate that each market currently receives 112 | mapping(address => uint) public compSpeeds; 113 | 114 | /// @notice The COMP market supply state for each market 115 | mapping(address => CompMarketState) public compSupplyState; 116 | 117 | /// @notice The COMP market borrow state for each market 118 | mapping(address => CompMarketState) public compBorrowState; 119 | 120 | /// @notice The COMP borrow index for each market for each supplier as of the last time they accrued COMP 121 | mapping(address => mapping(address => uint)) public compSupplierIndex; 122 | 123 | /// @notice The COMP borrow index for each market for each borrower as of the last time they accrued COMP 124 | mapping(address => mapping(address => uint)) public compBorrowerIndex; 125 | 126 | /// @notice The COMP accrued but not yet transferred to each user 127 | mapping(address => uint) public compAccrued; 128 | } 129 | 130 | contract ComptrollerV4Storage is ComptrollerV3Storage { 131 | // @notice The borrowCapGuardian can set borrowCaps to any number for any market. Lowering the borrow cap could disable borrowing on the given market. 132 | address public borrowCapGuardian; 133 | 134 | // @notice Borrow caps enforced by borrowAllowed for each cToken address. Defaults to zero which corresponds to unlimited borrowing. 135 | mapping(address => uint) public borrowCaps; 136 | } 137 | 138 | contract ComptrollerV5Storage is ComptrollerV4Storage { 139 | /// @notice The portion of COMP that each contributor receives per block 140 | mapping(address => uint) public compContributorSpeeds; 141 | 142 | /// @notice Last block at which a contributor's COMP rewards have been allocated 143 | mapping(address => uint) public lastContributorBlock; 144 | } 145 | 146 | contract ComptrollerV6Storage is ComptrollerV5Storage { 147 | /// @notice The rate at which comp is distributed to the corresponding borrow market (per block) 148 | mapping(address => uint) public compBorrowSpeeds; 149 | 150 | /// @notice The rate at which comp is distributed to the corresponding supply market (per block) 151 | mapping(address => uint) public compSupplySpeeds; 152 | } 153 | 154 | contract ComptrollerV7Storage is ComptrollerV6Storage { 155 | /// @notice Flag indicating whether the function to fix COMP accruals has been executed (RE: proposal 62 bug) 156 | bool public proposal65FixExecuted; 157 | 158 | /// @notice Accounting storage mapping account addresses to how much COMP they owe the protocol. 159 | mapping(address => uint) public compReceivable; 160 | } 161 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/Unitroller.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./ErrorReporter.sol"; 5 | import "./ComptrollerStorage.sol"; 6 | /** 7 | * @title ComptrollerCore 8 | * @dev Storage for the comptroller is at this address, while execution is delegated to the `comptrollerImplementation`. 9 | * CTokens should reference this contract as their comptroller. 10 | */ 11 | contract Unitroller is UnitrollerAdminStorage, ComptrollerErrorReporter { 12 | 13 | /** 14 | * @notice Emitted when pendingComptrollerImplementation is changed 15 | */ 16 | event NewPendingImplementation(address oldPendingImplementation, address newPendingImplementation); 17 | 18 | /** 19 | * @notice Emitted when pendingComptrollerImplementation is accepted, which means comptroller implementation is updated 20 | */ 21 | event NewImplementation(address oldImplementation, address newImplementation); 22 | 23 | /** 24 | * @notice Emitted when pendingAdmin is changed 25 | */ 26 | event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); 27 | 28 | /** 29 | * @notice Emitted when pendingAdmin is accepted, which means admin is updated 30 | */ 31 | event NewAdmin(address oldAdmin, address newAdmin); 32 | 33 | constructor() public { 34 | // Set admin to caller 35 | admin = msg.sender; 36 | } 37 | 38 | /*** Admin Functions ***/ 39 | function _setPendingImplementation(address newPendingImplementation) public returns (uint) { 40 | 41 | if (msg.sender != admin) { 42 | return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_IMPLEMENTATION_OWNER_CHECK); 43 | } 44 | 45 | address oldPendingImplementation = pendingComptrollerImplementation; 46 | 47 | pendingComptrollerImplementation = newPendingImplementation; 48 | 49 | emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); 50 | 51 | return uint(Error.NO_ERROR); 52 | } 53 | 54 | /** 55 | * @notice Accepts new implementation of comptroller. msg.sender must be pendingImplementation 56 | * @dev Admin function for new implementation to accept it's role as implementation 57 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 58 | */ 59 | function _acceptImplementation() public returns (uint) { 60 | // Check caller is pendingImplementation and pendingImplementation ≠ address(0) 61 | if (msg.sender != pendingComptrollerImplementation || pendingComptrollerImplementation == address(0)) { 62 | return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK); 63 | } 64 | 65 | // Save current values for inclusion in log 66 | address oldImplementation = comptrollerImplementation; 67 | address oldPendingImplementation = pendingComptrollerImplementation; 68 | 69 | comptrollerImplementation = pendingComptrollerImplementation; 70 | 71 | pendingComptrollerImplementation = address(0); 72 | 73 | emit NewImplementation(oldImplementation, comptrollerImplementation); 74 | emit NewPendingImplementation(oldPendingImplementation, pendingComptrollerImplementation); 75 | 76 | return uint(Error.NO_ERROR); 77 | } 78 | 79 | 80 | /** 81 | * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. 82 | * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. 83 | * @param newPendingAdmin New pending admin. 84 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 85 | */ 86 | function _setPendingAdmin(address newPendingAdmin) public returns (uint) { 87 | // Check caller = admin 88 | if (msg.sender != admin) { 89 | return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); 90 | } 91 | 92 | // Save current value, if any, for inclusion in log 93 | address oldPendingAdmin = pendingAdmin; 94 | 95 | // Store pendingAdmin with value newPendingAdmin 96 | pendingAdmin = newPendingAdmin; 97 | 98 | // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) 99 | emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); 100 | 101 | return uint(Error.NO_ERROR); 102 | } 103 | 104 | /** 105 | * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin 106 | * @dev Admin function for pending admin to accept role and update admin 107 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 108 | */ 109 | function _acceptAdmin() public returns (uint) { 110 | // Check caller is pendingAdmin and pendingAdmin ≠ address(0) 111 | if (msg.sender != pendingAdmin || msg.sender == address(0)) { 112 | return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); 113 | } 114 | 115 | // Save current values for inclusion in log 116 | address oldAdmin = admin; 117 | address oldPendingAdmin = pendingAdmin; 118 | 119 | // Store admin with value pendingAdmin 120 | admin = pendingAdmin; 121 | 122 | // Clear the pending value 123 | pendingAdmin = address(0); 124 | 125 | emit NewAdmin(oldAdmin, admin); 126 | emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); 127 | 128 | return uint(Error.NO_ERROR); 129 | } 130 | 131 | /** 132 | * @dev Delegates execution to an implementation contract. 133 | * It returns to the external caller whatever the implementation returns 134 | * or forwards reverts. 135 | */ 136 | fallback() payable external { 137 | // delegate all other functions to current implementation 138 | (bool success, ) = comptrollerImplementation.delegatecall(msg.data); 139 | 140 | assembly { 141 | let free_mem_ptr := mload(0x40) 142 | returndatacopy(free_mem_ptr, 0, returndatasize()) 143 | 144 | switch success 145 | case 0 { revert(free_mem_ptr, returndatasize()) } 146 | default { return(free_mem_ptr, returndatasize()) } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/ExponentialNoError.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | /** 5 | * @title Exponential module for storing fixed-precision decimals 6 | * @author Compound 7 | * @notice Exp is a struct which stores decimals with a fixed precision of 18 decimal places. 8 | * Thus, if we wanted to store the 5.1, mantissa would store 5.1e18. That is: 9 | * `Exp({mantissa: 5100000000000000000})`. 10 | */ 11 | contract ExponentialNoError { 12 | uint constant expScale = 1e18; 13 | uint constant doubleScale = 1e36; 14 | uint constant halfExpScale = expScale/2; 15 | uint constant mantissaOne = expScale; 16 | 17 | struct Exp { 18 | uint mantissa; 19 | } 20 | 21 | struct Double { 22 | uint mantissa; 23 | } 24 | 25 | /** 26 | * @dev Truncates the given exp to a whole number value. 27 | * For example, truncate(Exp{mantissa: 15 * expScale}) = 15 28 | */ 29 | function truncate(Exp memory exp) pure internal returns (uint) { 30 | // Note: We are not using careful math here as we're performing a division that cannot fail 31 | return exp.mantissa / expScale; 32 | } 33 | 34 | /** 35 | * @dev Multiply an Exp by a scalar, then truncate to return an unsigned integer. 36 | */ 37 | function mul_ScalarTruncate(Exp memory a, uint scalar) pure internal returns (uint) { 38 | Exp memory product = mul_(a, scalar); 39 | return truncate(product); 40 | } 41 | 42 | /** 43 | * @dev Multiply an Exp by a scalar, truncate, then add an to an unsigned integer, returning an unsigned integer. 44 | */ 45 | function mul_ScalarTruncateAddUInt(Exp memory a, uint scalar, uint addend) pure internal returns (uint) { 46 | Exp memory product = mul_(a, scalar); 47 | return add_(truncate(product), addend); 48 | } 49 | 50 | /** 51 | * @dev Checks if first Exp is less than second Exp. 52 | */ 53 | function lessThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { 54 | return left.mantissa < right.mantissa; 55 | } 56 | 57 | /** 58 | * @dev Checks if left Exp <= right Exp. 59 | */ 60 | function lessThanOrEqualExp(Exp memory left, Exp memory right) pure internal returns (bool) { 61 | return left.mantissa <= right.mantissa; 62 | } 63 | 64 | /** 65 | * @dev Checks if left Exp > right Exp. 66 | */ 67 | function greaterThanExp(Exp memory left, Exp memory right) pure internal returns (bool) { 68 | return left.mantissa > right.mantissa; 69 | } 70 | 71 | /** 72 | * @dev returns true if Exp is exactly zero 73 | */ 74 | function isZeroExp(Exp memory value) pure internal returns (bool) { 75 | return value.mantissa == 0; 76 | } 77 | 78 | function safe224(uint n, string memory errorMessage) pure internal returns (uint224) { 79 | require(n < 2**224, errorMessage); 80 | return uint224(n); 81 | } 82 | 83 | function safe32(uint n, string memory errorMessage) pure internal returns (uint32) { 84 | require(n < 2**32, errorMessage); 85 | return uint32(n); 86 | } 87 | 88 | function add_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { 89 | return Exp({mantissa: add_(a.mantissa, b.mantissa)}); 90 | } 91 | 92 | function add_(Double memory a, Double memory b) pure internal returns (Double memory) { 93 | return Double({mantissa: add_(a.mantissa, b.mantissa)}); 94 | } 95 | 96 | function add_(uint a, uint b) pure internal returns (uint) { 97 | return a + b; 98 | } 99 | 100 | function sub_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { 101 | return Exp({mantissa: sub_(a.mantissa, b.mantissa)}); 102 | } 103 | 104 | function sub_(Double memory a, Double memory b) pure internal returns (Double memory) { 105 | return Double({mantissa: sub_(a.mantissa, b.mantissa)}); 106 | } 107 | 108 | function sub_(uint a, uint b) pure internal returns (uint) { 109 | return a - b; 110 | } 111 | 112 | function mul_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { 113 | return Exp({mantissa: mul_(a.mantissa, b.mantissa) / expScale}); 114 | } 115 | 116 | function mul_(Exp memory a, uint b) pure internal returns (Exp memory) { 117 | return Exp({mantissa: mul_(a.mantissa, b)}); 118 | } 119 | 120 | function mul_(uint a, Exp memory b) pure internal returns (uint) { 121 | return mul_(a, b.mantissa) / expScale; 122 | } 123 | 124 | function mul_(Double memory a, Double memory b) pure internal returns (Double memory) { 125 | return Double({mantissa: mul_(a.mantissa, b.mantissa) / doubleScale}); 126 | } 127 | 128 | function mul_(Double memory a, uint b) pure internal returns (Double memory) { 129 | return Double({mantissa: mul_(a.mantissa, b)}); 130 | } 131 | 132 | function mul_(uint a, Double memory b) pure internal returns (uint) { 133 | return mul_(a, b.mantissa) / doubleScale; 134 | } 135 | 136 | function mul_(uint a, uint b) pure internal returns (uint) { 137 | return a * b; 138 | } 139 | 140 | function div_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { 141 | return Exp({mantissa: div_(mul_(a.mantissa, expScale), b.mantissa)}); 142 | } 143 | 144 | function div_(Exp memory a, uint b) pure internal returns (Exp memory) { 145 | return Exp({mantissa: div_(a.mantissa, b)}); 146 | } 147 | 148 | function div_(uint a, Exp memory b) pure internal returns (uint) { 149 | return div_(mul_(a, expScale), b.mantissa); 150 | } 151 | 152 | function div_(Double memory a, Double memory b) pure internal returns (Double memory) { 153 | return Double({mantissa: div_(mul_(a.mantissa, doubleScale), b.mantissa)}); 154 | } 155 | 156 | function div_(Double memory a, uint b) pure internal returns (Double memory) { 157 | return Double({mantissa: div_(a.mantissa, b)}); 158 | } 159 | 160 | function div_(uint a, Double memory b) pure internal returns (uint) { 161 | return div_(mul_(a, doubleScale), b.mantissa); 162 | } 163 | 164 | function div_(uint a, uint b) pure internal returns (uint) { 165 | return a / b; 166 | } 167 | 168 | function fraction(uint a, uint b) pure internal returns (Double memory) { 169 | return Double({mantissa: div_(mul_(a, doubleScale), b)}); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/web3test/ape/fixtures/erc20.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyTest Fixtures. 3 | """ 4 | 5 | import json 6 | from pathlib import Path 7 | from typing import Iterator 8 | 9 | import pytest 10 | from web3.types import ABI 11 | 12 | import ape 13 | from web3test.ape.helpers.token import deploy_token 14 | 15 | 16 | @pytest.fixture(scope="session") 17 | def erc20_abi_string() -> Iterator[str]: 18 | """The ABI for the ERC20 token standard, as a string""" 19 | yield '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]' 20 | 21 | 22 | @pytest.fixture(scope="function") 23 | def erc20_abi_file(tmp_path: Path, erc20_abi_string: str) -> Iterator[str]: 24 | """The path of a JSON file containing the ABI for the ERC20 token 25 | standard""" 26 | f = tmp_path / "erc20.json" 27 | f.write_text(erc20_abi_string) 28 | yield str(f) 29 | 30 | 31 | @pytest.fixture(scope="session") 32 | def erc20_abi(erc20_abi_string: str) -> Iterator[ABI]: 33 | """The ABI for the ERC20 token standard, as a JSON object""" 34 | yield json.loads(erc20_abi_string) 35 | 36 | 37 | @pytest.fixture(scope="function") 38 | def Token() -> ape.contracts.ContractContainer: 39 | """The token contract container, which can be used to deploy token 40 | contracts on the local chain""" 41 | return ape.project.get_contract("Token") 42 | 43 | 44 | @pytest.fixture(scope="function") 45 | def WETH( 46 | accounts: ape.managers.accounts.AccountManager, 47 | Token: ape.contracts.ContractContainer, 48 | ) -> ape.contracts.ContractInstance: 49 | """A token deployed on the local chain, with 18 decimals, that 50 | we will use as if it were WETH. Supply of 1 billion tokens, shared between 51 | all accounts.""" 52 | return deploy_token( 53 | accounts, 54 | Token, 55 | f"WrappedEther", 56 | f"WETH", 57 | 18, 58 | 10**9, 59 | True, 60 | ) 61 | 62 | 63 | @pytest.fixture(scope="function") 64 | def TST( 65 | accounts: ape.managers.accounts.AccountManager, 66 | Token: ape.contracts.ContractContainer, 67 | ) -> ape.contracts.ContractInstance: 68 | """TST token deployed on the local chain, with 18 decimals. 69 | Supply of 1 billion tokens, shared between all accounts.""" 70 | return deploy_token( 71 | accounts, 72 | Token, 73 | f"Test token (18 decimals)", 74 | f"TST", 75 | 18, 76 | 10**9, 77 | True, 78 | ) 79 | 80 | 81 | @pytest.fixture(scope="function") 82 | def TST_0( 83 | accounts: ape.managers.accounts.AccountManager, 84 | Token: ape.contracts.ContractContainer, 85 | ) -> ape.contracts.ContractInstance: 86 | """TST_0 token deployed on the local chain, with 18 decimals. 87 | Supply of 1 billion tokens, shared between all accounts.""" 88 | return deploy_token( 89 | accounts, 90 | Token, 91 | f"Test token 0 (18 decimals)", 92 | f"TST_0", 93 | 18, 94 | 10**9, 95 | True, 96 | ) 97 | 98 | 99 | @pytest.fixture(scope="function") 100 | def TST_1( 101 | accounts: ape.managers.accounts.AccountManager, 102 | Token: ape.contracts.ContractContainer, 103 | ) -> ape.contracts.ContractInstance: 104 | """TST_1 token deployed on the local chain, with 18 decimals. 105 | Supply of 1 billion tokens, shared between all accounts.""" 106 | return deploy_token( 107 | accounts, 108 | Token, 109 | f"Test token 1 (18 decimals)", 110 | f"TST_1", 111 | 18, 112 | 10**9, 113 | True, 114 | ) 115 | 116 | 117 | @pytest.fixture(scope="function") 118 | def TST6( 119 | accounts: ape.managers.accounts.AccountManager, 120 | Token: ape.contracts.ContractContainer, 121 | ) -> ape.contracts.ContractInstance: 122 | """TST6 token deployed on the local chain, with 6 decimals. 123 | Supply of 1 billion tokens, shared between all accounts""" 124 | return deploy_token( 125 | accounts, 126 | Token, 127 | f"Test token (6 decimals)", 128 | f"TST6", 129 | 6, 130 | 10**9, 131 | True, 132 | ) 133 | 134 | 135 | @pytest.fixture(scope="function") 136 | def TST6_0( 137 | accounts: ape.managers.accounts.AccountManager, 138 | Token: ape.contracts.ContractContainer, 139 | ) -> ape.contracts.ContractInstance: 140 | """TST6_0 token deployed on the local chain, with 6 141 | decimals; each account will have a billion tokens""" 142 | return deploy_token( 143 | accounts, 144 | Token, 145 | f"Test token 0 (6 decimals)", 146 | f"TST6_0", 147 | 6, 148 | 10**9, 149 | True, 150 | ) 151 | 152 | 153 | @pytest.fixture(scope="function") 154 | def TST6_1( 155 | accounts: ape.managers.accounts.AccountManager, 156 | Token: ape.contracts.ContractContainer, 157 | ) -> ape.contracts.ContractInstance: 158 | """TST6_1 token deployed on the local chain, with 6 159 | decimals; each account will have a billion tokens""" 160 | return deploy_token( 161 | accounts, 162 | Token, 163 | f"Test token 1 (6 decimals)", 164 | f"TST6_1", 165 | 6, 166 | 10**9, 167 | True, 168 | ) 169 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/SafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | // From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol 5 | // Subject to the MIT license. 6 | 7 | /** 8 | * @dev Wrappers over Solidity's arithmetic operations with added overflow 9 | * checks. 10 | * 11 | * Arithmetic operations in Solidity wrap on overflow. This can easily result 12 | * in bugs, because programmers usually assume that an overflow raises an 13 | * error, which is the standard behavior in high level programming languages. 14 | * `SafeMath` restores this intuition by reverting the transaction when an 15 | * operation overflows. 16 | * 17 | * Using this library instead of the unchecked operations eliminates an entire 18 | * class of bugs, so it's recommended to use it always. 19 | */ 20 | library SafeMath { 21 | /** 22 | * @dev Returns the addition of two unsigned integers, reverting on overflow. 23 | * 24 | * Counterpart to Solidity's `+` operator. 25 | * 26 | * Requirements: 27 | * - Addition cannot overflow. 28 | */ 29 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 30 | uint256 c; 31 | unchecked { c = a + b; } 32 | require(c >= a, "SafeMath: addition overflow"); 33 | 34 | return c; 35 | } 36 | 37 | /** 38 | * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. 39 | * 40 | * Counterpart to Solidity's `+` operator. 41 | * 42 | * Requirements: 43 | * - Addition cannot overflow. 44 | */ 45 | function add(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 46 | uint256 c; 47 | unchecked { c = a + b; } 48 | require(c >= a, errorMessage); 49 | 50 | return c; 51 | } 52 | 53 | /** 54 | * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). 55 | * 56 | * Counterpart to Solidity's `-` operator. 57 | * 58 | * Requirements: 59 | * - Subtraction cannot underflow. 60 | */ 61 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 62 | return sub(a, b, "SafeMath: subtraction underflow"); 63 | } 64 | 65 | /** 66 | * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). 67 | * 68 | * Counterpart to Solidity's `-` operator. 69 | * 70 | * Requirements: 71 | * - Subtraction cannot underflow. 72 | */ 73 | function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 74 | require(b <= a, errorMessage); 75 | uint256 c = a - b; 76 | 77 | return c; 78 | } 79 | 80 | /** 81 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 82 | * 83 | * Counterpart to Solidity's `*` operator. 84 | * 85 | * Requirements: 86 | * - Multiplication cannot overflow. 87 | */ 88 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 89 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 90 | // benefit is lost if 'b' is also tested. 91 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 92 | if (a == 0) { 93 | return 0; 94 | } 95 | 96 | uint256 c; 97 | unchecked { c = a * b; } 98 | require(c / a == b, "SafeMath: multiplication overflow"); 99 | 100 | return c; 101 | } 102 | 103 | /** 104 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 105 | * 106 | * Counterpart to Solidity's `*` operator. 107 | * 108 | * Requirements: 109 | * - Multiplication cannot overflow. 110 | */ 111 | function mul(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 112 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 113 | // benefit is lost if 'b' is also tested. 114 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 115 | if (a == 0) { 116 | return 0; 117 | } 118 | 119 | uint256 c; 120 | unchecked { c = a * b; } 121 | require(c / a == b, errorMessage); 122 | 123 | return c; 124 | } 125 | 126 | /** 127 | * @dev Returns the integer division of two unsigned integers. 128 | * Reverts on division by zero. The result is rounded towards zero. 129 | * 130 | * Counterpart to Solidity's `/` operator. Note: this function uses a 131 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 132 | * uses an invalid opcode to revert (consuming all remaining gas). 133 | * 134 | * Requirements: 135 | * - The divisor cannot be zero. 136 | */ 137 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 138 | return div(a, b, "SafeMath: division by zero"); 139 | } 140 | 141 | /** 142 | * @dev Returns the integer division of two unsigned integers. 143 | * Reverts with custom message on division by zero. The result is rounded towards zero. 144 | * 145 | * Counterpart to Solidity's `/` operator. Note: this function uses a 146 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 147 | * uses an invalid opcode to revert (consuming all remaining gas). 148 | * 149 | * Requirements: 150 | * - The divisor cannot be zero. 151 | */ 152 | function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 153 | // Solidity only automatically asserts when dividing by 0 154 | require(b > 0, errorMessage); 155 | uint256 c = a / b; 156 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 157 | 158 | return c; 159 | } 160 | 161 | /** 162 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 163 | * Reverts when dividing by zero. 164 | * 165 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 166 | * opcode (which leaves remaining gas untouched) while Solidity uses an 167 | * invalid opcode to revert (consuming all remaining gas). 168 | * 169 | * Requirements: 170 | * - The divisor cannot be zero. 171 | */ 172 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 173 | return mod(a, b, "SafeMath: modulo by zero"); 174 | } 175 | 176 | /** 177 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 178 | * Reverts with custom message when dividing by zero. 179 | * 180 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 181 | * opcode (which leaves remaining gas untouched) while Solidity uses an 182 | * invalid opcode to revert (consuming all remaining gas). 183 | * 184 | * Requirements: 185 | * - The divisor cannot be zero. 186 | */ 187 | function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 188 | require(b != 0, errorMessage); 189 | return a % b; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/web3client/erc20_client.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from functools import cached_property 3 | from typing import Any 4 | 5 | from eth_typing import HexStr 6 | from typing_extensions import Self 7 | from web3 import Web3 8 | from web3.types import Nonce 9 | 10 | from web3client.base_client import BaseClient 11 | from web3client.exceptions import Web3ClientException 12 | 13 | 14 | class Erc20Client(BaseClient): 15 | """ 16 | Client that comes with the ERC20 ABI preloaded. 17 | 18 | It allows you to call the contract functions as class methods: 19 | transfer, balanceOf, name, etc. 20 | 21 | If you pass a private key, it will allow you to call the 22 | write functions (transfer, approve, etc.) 23 | 24 | The token properties (name, symbol, total_supply, decimals) can be 25 | accessed as attributes, and are cached. 26 | 27 | AMOUNTS 28 | ======= 29 | 30 | Whenever we will refer to an "amount" of the token, we really mean an 31 | amount in the smallest subdivision of the token (e.g. wei). For example: 32 | - If the token has 6 decimals (like most stablecoins) an amount of 1 33 | corresponds to one millionth of the token. 34 | - For tokens with 18 decimals (like most non-stablecoins) an amount 35 | of 1 is equal to 1/10^18 of the token (a single wei). 36 | """ 37 | 38 | abi = BaseClient.get_abi_json("erc20.json") 39 | 40 | #################### 41 | # Read 42 | #################### 43 | 44 | def balance(self, address: str = None) -> Decimal: 45 | """ 46 | Return the amount of the ERC20 token held by the given 47 | address; if no address is specified, return the amount 48 | held by the client's account 49 | """ 50 | balance_in_wei = self.balance_in_wei(address) 51 | return self.from_wei(balance_in_wei, self.decimals) 52 | 53 | def balance_in_wei(self, address: str = None) -> int: 54 | """ 55 | Return the amount of the ERC20 token held by the given address, 56 | in wei; if no address is specified, return the amount held by 57 | the client's account 58 | """ 59 | if not address: 60 | address = self.account.address 61 | return self.functions.balanceOf(Web3.to_checksum_address(address)).call() 62 | 63 | def total_supply(self) -> int: 64 | """ 65 | Return the total supply of the token 66 | """ 67 | return self.functions.totalSupply().call() 68 | 69 | @cached_property 70 | def name(self) -> str: 71 | """ 72 | Return the name/label of the token 73 | """ 74 | return self.functions.name().call() 75 | 76 | @cached_property 77 | def symbol(self) -> str: 78 | """ 79 | Return the symbol/ticker of the token 80 | """ 81 | return self.functions.symbol().call() 82 | 83 | @cached_property 84 | def decimals(self) -> int: 85 | """ 86 | Return the number of digits of the token 87 | """ 88 | return self.functions.decimals().call() 89 | 90 | #################### 91 | # Write 92 | #################### 93 | 94 | def transfer( 95 | self, 96 | to: str, 97 | amount: int, 98 | value_in_wei: int = None, 99 | nonce: Nonce = None, 100 | gas_limit: int = None, 101 | max_priority_fee_in_gwei: float = None, 102 | ) -> HexStr: 103 | """ 104 | Transfer some amount of the token to an address; does not 105 | require approval. 106 | """ 107 | return self.transact( 108 | self.functions.transfer(Web3.to_checksum_address(to), amount), 109 | value_in_wei, 110 | nonce, 111 | gas_limit, 112 | max_priority_fee_in_gwei, 113 | ) 114 | 115 | def approve( 116 | self, 117 | spender: str, 118 | amount: int, 119 | value_in_wei: int = None, 120 | nonce: Nonce = None, 121 | gas_limit: int = None, 122 | max_priority_fee_in_gwei: float = None, 123 | ) -> HexStr: 124 | """ 125 | Approve the given address to spend some amount of the token 126 | on behalf of the sender. 127 | """ 128 | return self.transact( 129 | self.functions.approve(Web3.to_checksum_address(spender), amount), 130 | value_in_wei, 131 | nonce, 132 | gas_limit, 133 | max_priority_fee_in_gwei, 134 | ) 135 | 136 | @staticmethod 137 | def from_wei(amount: int, decimals: int) -> Decimal: 138 | """ 139 | Given an amount in wei, return the equivalent amount in 140 | token units. Here by wei we mean the smallest subdivision 141 | of the token (e.g. 1/10**6 for USDC, 1/10**18 for UNI). 142 | """ 143 | return amount / Decimal(10**decimals) 144 | 145 | 146 | class DualClient(Erc20Client): 147 | """A client that works with either ERC20 tokens or native 148 | coins such as ETH, BNB, etc. 149 | 150 | Set the contract address to an ERC20 token, and the client will 151 | behave exactly like an Erc20Client. 152 | 153 | Set the contract address to "native" and you will be able to use 154 | the same method of the Erc20Client, but the "token" will be the 155 | blockchain native coin. 156 | """ 157 | 158 | def set_contract(self, contract_address: str, abi: dict[str, Any] = None) -> Self: 159 | if contract_address == "native": 160 | self.contract_address = "native" 161 | return self 162 | return super().set_contract(contract_address, abi) 163 | 164 | #################### 165 | # Read 166 | #################### 167 | 168 | def is_native(self) -> bool: 169 | return self.contract_address == "native" 170 | 171 | def is_erc20(self) -> bool: 172 | return bool(self.contract_address) and self.contract_address != "native" 173 | 174 | def balance(self, address: str = None) -> Decimal: 175 | if self.is_native(): 176 | return Decimal(self.get_balance_in_eth(address)) 177 | return super().balance(address) 178 | 179 | def balance_in_wei(self, address: str = None) -> int: 180 | if self.is_native(): 181 | return self.get_balance_in_wei(address) 182 | return super().balance_in_wei(address) 183 | 184 | def total_supply(self) -> int: 185 | if self.is_native(): 186 | raise Web3ClientException(f"Cannot get total supply of native coin") 187 | return super().total_supply() 188 | 189 | @cached_property 190 | def name(self) -> str: 191 | if self.is_native(): 192 | return "Native coin" 193 | return super().name 194 | 195 | @cached_property 196 | def symbol(self) -> str: 197 | if self.is_native(): 198 | return "Native coin" 199 | return super().symbol 200 | 201 | @cached_property 202 | def decimals(self) -> int: 203 | if self.is_native(): 204 | return 18 205 | return super().decimals 206 | 207 | #################### 208 | # Write 209 | #################### 210 | 211 | def transfer( 212 | self, 213 | to: str, 214 | amount: int, 215 | value_in_wei: int = None, 216 | nonce: Nonce = None, 217 | gas_limit: int = None, 218 | max_priority_fee_in_gwei: float = None, 219 | ) -> HexStr: 220 | if self.is_native(): 221 | if value_in_wei: 222 | raise Web3ClientException( 223 | "Cannot specify 'value_in_wei' when transferring native coins, use 'amount' instead" 224 | ) 225 | return self.send_eth_in_wei( 226 | to=to, 227 | value_in_wei=amount, 228 | nonce=nonce, 229 | gas_limit=gas_limit, 230 | max_priority_fee_in_gwei=max_priority_fee_in_gwei, 231 | ) 232 | return super().transfer( 233 | to, amount, value_in_wei, nonce, gas_limit, max_priority_fee_in_gwei 234 | ) 235 | 236 | def approve( 237 | self, 238 | spender: str, 239 | amount: int, 240 | value_in_wei: int = None, 241 | nonce: Nonce = None, 242 | gas_limit: int = None, 243 | max_priority_fee_in_gwei: float = None, 244 | strict: bool = True, 245 | ) -> HexStr: 246 | """ 247 | Approve the given address to spend some amount of the token 248 | on behalf of the sender. 249 | 250 | For native coins, if strict is True, it will raise an exception 251 | when you try to approve native coins. Otherwise, nothing will happen 252 | and the function will return None. 253 | """ 254 | if self.is_native(): 255 | if strict: 256 | raise Web3ClientException("Cannot approve native coins") 257 | else: 258 | return None 259 | return self.transact( 260 | self.functions.approve(Web3.to_checksum_address(spender), amount), 261 | value_in_wei, 262 | nonce, 263 | gas_limit, 264 | max_priority_fee_in_gwei, 265 | ) 266 | -------------------------------------------------------------------------------- /tests/web3client/test_compound_v2_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import ape 4 | from web3client.compound_v2_client import ( 5 | CompoundV2CErc20Client, 6 | CompoundV2CEtherClient, 7 | CompoundV2ComptrollerClient, 8 | ) 9 | 10 | 11 | @pytest.mark.local 12 | def test_compound_v2_ctst_read(compound_v2_ctst_client: CompoundV2CErc20Client) -> None: 13 | assert compound_v2_ctst_client.symbol == "cTST" 14 | 15 | 16 | @pytest.mark.local 17 | def test_compound_v2_ceth_read(compound_v2_ceth_client: CompoundV2CErc20Client) -> None: 18 | assert compound_v2_ceth_client.symbol == "cETH" 19 | 20 | 21 | @pytest.mark.local 22 | def test_compound_v2_ctst_underlying_balance( 23 | alice_compound_v2_ctst_client: CompoundV2CErc20Client, 24 | alice: ape.api.AccountAPI, 25 | TST: ape.contracts.ContractInstance, 26 | ) -> None: 27 | client = alice_compound_v2_ctst_client 28 | assert TST.balanceOf(alice) == client.underlying_balance() 29 | 30 | 31 | @pytest.mark.local 32 | def test_compound_v2_ceth_underlying_balance( 33 | alice_compound_v2_ceth_client: CompoundV2CErc20Client, 34 | alice: ape.api.AccountAPI, 35 | ) -> None: 36 | client = alice_compound_v2_ceth_client 37 | assert alice.balance == client.underlying_balance() 38 | 39 | 40 | @pytest.mark.local 41 | def test_compound_v2_ctst_supply( 42 | alice_compound_v2_ctst_client: CompoundV2CErc20Client, 43 | ) -> None: 44 | client = alice_compound_v2_ctst_client 45 | alice_balance = client.get_underlying_client().balance_in_wei() 46 | amount = 3 * 10**18 47 | client.approve_and_supply(amount) 48 | assert client.supplied() == amount 49 | assert client.get_underlying_client().balance_in_wei() == alice_balance - amount 50 | assert client.total_supply() * client.exchange_rate() == amount * 10**18 51 | assert client.liquidity() == amount 52 | assert client.solvency() == float("inf") 53 | 54 | 55 | @pytest.mark.local 56 | def test_compound_v2_ceth_supply( 57 | alice_compound_v2_ceth_client: CompoundV2CEtherClient, 58 | ) -> None: 59 | client = alice_compound_v2_ceth_client 60 | exchange_rate = client.exchange_rate() 61 | amount = 10**18 62 | client.supply(amount) 63 | assert client.supplied() == amount 64 | assert client.total_supply() * exchange_rate == amount * 10**18 65 | assert client.liquidity() == amount 66 | assert client.solvency() == float("inf") 67 | 68 | 69 | @pytest.mark.local 70 | def test_compound_v2_ctst_borrow( 71 | alice_compound_v2_ctst_client: CompoundV2CErc20Client, 72 | alice_compound_v2_comptroller_client: CompoundV2ComptrollerClient, 73 | ) -> None: 74 | client = alice_compound_v2_ctst_client 75 | alice_balance = client.get_underlying_client().balance_in_wei() 76 | # Supply 77 | supply_amount = 3 * 10**18 78 | client.approve_and_supply(supply_amount) 79 | # Enable collateral 80 | alice_compound_v2_comptroller_client.enter_market(client.contract_address) 81 | # Borrow 82 | borrow_amount = supply_amount // 3 83 | client.borrow(borrow_amount) 84 | # Check balance 85 | assert client.borrowed() == borrow_amount 86 | assert client.total_borrowed() == borrow_amount 87 | assert client.liquidity() == supply_amount - borrow_amount 88 | assert ( 89 | client.get_underlying_client().balance_in_wei() 90 | == alice_balance + borrow_amount - supply_amount 91 | ) 92 | 93 | 94 | @pytest.mark.local 95 | def test_compound_v2_ceth_borrow( 96 | alice_compound_v2_ceth_client: CompoundV2CEtherClient, 97 | alice_compound_v2_comptroller_client: CompoundV2ComptrollerClient, 98 | ) -> None: 99 | client = alice_compound_v2_ceth_client 100 | alice_balance = client.get_balance_in_wei() 101 | # Supply 102 | supply_amount = 3 * 10**18 103 | tx1 = client.supply(supply_amount) 104 | rcpt1 = client.get_tx_receipt(tx1) 105 | # Enable collateral 106 | tx2 = alice_compound_v2_comptroller_client.enter_market(client.contract_address) 107 | rcpt2 = client.get_tx_receipt(tx2) 108 | # Borrow 109 | borrow_amount = supply_amount // 3 110 | tx3 = client.borrow(borrow_amount) 111 | rcpt3 = client.get_tx_receipt(tx3) 112 | # Check balance 113 | assert client.borrowed() == borrow_amount 114 | assert client.total_borrowed() == borrow_amount 115 | assert client.liquidity() == supply_amount - borrow_amount 116 | assert ( 117 | client.get_balance_in_wei() 118 | == alice_balance 119 | + borrow_amount 120 | - supply_amount 121 | - rcpt1["gasUsed"] * rcpt1["effectiveGasPrice"] 122 | - rcpt2["gasUsed"] * rcpt2["effectiveGasPrice"] 123 | - rcpt3["gasUsed"] * rcpt3["effectiveGasPrice"] 124 | ) 125 | 126 | 127 | @pytest.mark.local 128 | def test_compound_v2_ctst_withdraw( 129 | alice_compound_v2_ctst_client: CompoundV2CErc20Client, 130 | ) -> None: 131 | client = alice_compound_v2_ctst_client 132 | alice_balance = client.get_underlying_client().balance_in_wei() 133 | # Supply 134 | supply_amount = 3 * 10**18 135 | client.approve_and_supply(supply_amount) 136 | # Withdraw half 137 | withdraw_amount = supply_amount // 2 138 | client.withdraw(withdraw_amount) 139 | # Check balance 140 | assert ( 141 | client.get_underlying_client().balance_in_wei() 142 | == alice_balance + withdraw_amount - supply_amount 143 | ) 144 | # Witdhraw the rest 145 | client.withdraw_all() 146 | assert client.get_underlying_client().balance_in_wei() == alice_balance 147 | assert client.liquidity() == 0 148 | assert client.solvency() == 0 149 | 150 | 151 | @pytest.mark.local 152 | def test_compound_v2_ceth_withdraw( 153 | alice_compound_v2_ceth_client: CompoundV2CErc20Client, 154 | ) -> None: 155 | client = alice_compound_v2_ceth_client 156 | alice_balance = client.get_balance_in_wei() 157 | # Supply 158 | supply_amount = 3 * 10**18 159 | tx1 = client.supply(supply_amount) 160 | rcpt1 = client.get_tx_receipt(tx1) 161 | # Withdraw half 162 | withdraw_amount = supply_amount // 2 163 | tx2 = client.withdraw(withdraw_amount) 164 | rcpt2 = client.get_tx_receipt(tx2) 165 | # Check balance 166 | assert ( 167 | client.get_balance_in_wei() 168 | == alice_balance 169 | + withdraw_amount 170 | - supply_amount 171 | - rcpt1["gasUsed"] * rcpt1["effectiveGasPrice"] 172 | - rcpt2["gasUsed"] * rcpt2["effectiveGasPrice"] 173 | ) 174 | # Witdhraw the rest 175 | tx3 = client.withdraw_all() 176 | rcpt3 = client.get_tx_receipt(tx3) 177 | assert ( 178 | client.get_balance_in_wei() 179 | == alice_balance 180 | - rcpt1["gasUsed"] * rcpt1["effectiveGasPrice"] 181 | - rcpt2["gasUsed"] * rcpt2["effectiveGasPrice"] 182 | - rcpt3["gasUsed"] * rcpt3["effectiveGasPrice"] 183 | ) 184 | assert client.liquidity() == 0 185 | assert client.solvency() == 0 186 | 187 | 188 | @pytest.mark.local 189 | def test_compound_v2_ctst_repay( 190 | alice_compound_v2_ctst_client: CompoundV2CErc20Client, 191 | alice_compound_v2_comptroller_client: CompoundV2ComptrollerClient, 192 | ) -> None: 193 | client = alice_compound_v2_ctst_client 194 | # Supply 195 | supply_amount = 3 * 10**18 196 | client.approve_and_supply(supply_amount) 197 | # Borrow 198 | alice_compound_v2_comptroller_client.enter_market(client.contract_address) 199 | borrow_amount = supply_amount // 3 200 | client.borrow(borrow_amount) 201 | # Repay a small amount 202 | repay_amount = borrow_amount // 10 203 | client.approve_and_repay(repay_amount) 204 | # Make sure that the amount borrowed has decreased 205 | # The comparison is approximate because of the interest 206 | tolerance = 1e-4 207 | assert abs(borrow_amount - client.borrowed()) / repay_amount - 1 < tolerance 208 | assert client.liquidity() == supply_amount - borrow_amount + repay_amount 209 | assert ( 210 | abs(client.solvency() - client.liquidity() / client.total_borrowed()) 211 | < tolerance 212 | ) 213 | # Repay the remaining amount 214 | client.approve_and_repay_all() 215 | assert ( 216 | client.borrowed() == 0 217 | ) # repay-all reduces borrowed amount to zero for ec20 tokens 218 | assert ( 219 | abs(client.liquidity() - supply_amount) 220 | < tolerance * 10 ** client.get_underlying_client().decimals 221 | ) 222 | 223 | 224 | @pytest.mark.local 225 | def test_compound_v2_ceth_repay( 226 | alice_compound_v2_ceth_client: CompoundV2CErc20Client, 227 | alice_compound_v2_comptroller_client: CompoundV2ComptrollerClient, 228 | ) -> None: 229 | client = alice_compound_v2_ceth_client 230 | # Supply 231 | supply_amount = 3 * 10**18 232 | client.supply(supply_amount) 233 | # Borrow 234 | alice_compound_v2_comptroller_client.enter_market(client.contract_address) 235 | borrow_amount = supply_amount // 3 236 | client.borrow(borrow_amount) 237 | # Repay a small amount 238 | repay_amount = borrow_amount // 10 239 | client.repay(repay_amount) 240 | # Make sure that the amount borrowed has decreased 241 | # The comparison is approximate because of the interest 242 | tolerance = 1e-4 243 | assert abs(borrow_amount - client.borrowed()) / repay_amount - 1 < tolerance 244 | assert client.liquidity() == supply_amount - borrow_amount + repay_amount 245 | assert ( 246 | abs(client.solvency() - client.liquidity() / client.total_borrowed()) 247 | < tolerance 248 | ) 249 | # Repay the remaining amount 250 | client.repay_all() 251 | assert ( 252 | client.borrowed() < tolerance * borrow_amount 253 | ) # repay-all always leaves some dust in borrowed amount for ETH 254 | assert abs(client.liquidity() - supply_amount) < tolerance * 10**18 255 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/CErc20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./CToken.sol"; 5 | 6 | interface CompLike { 7 | function delegate(address delegatee) external; 8 | } 9 | 10 | /** 11 | * @title Compound's CErc20 Contract 12 | * @notice CTokens which wrap an EIP-20 underlying 13 | * @author Compound 14 | */ 15 | contract CErc20 is CToken, CErc20Interface { 16 | /** 17 | * @notice Initialize the new money market 18 | * @param underlying_ The address of the underlying asset 19 | * @param comptroller_ The address of the Comptroller 20 | * @param interestRateModel_ The address of the interest rate model 21 | * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 22 | * @param name_ ERC-20 name of this token 23 | * @param symbol_ ERC-20 symbol of this token 24 | * @param decimals_ ERC-20 decimal precision of this token 25 | */ 26 | function initialize(address underlying_, 27 | ComptrollerInterface comptroller_, 28 | InterestRateModel interestRateModel_, 29 | uint initialExchangeRateMantissa_, 30 | string memory name_, 31 | string memory symbol_, 32 | uint8 decimals_) public { 33 | // CToken initialize does the bulk of the work 34 | super.initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); 35 | 36 | // Set underlying and sanity check it 37 | underlying = underlying_; 38 | EIP20Interface(underlying).totalSupply(); 39 | } 40 | 41 | /*** User Interface ***/ 42 | 43 | /** 44 | * @notice Sender supplies assets into the market and receives cTokens in exchange 45 | * @dev Accrues interest whether or not the operation succeeds, unless reverted 46 | * @param mintAmount The amount of the underlying asset to supply 47 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 48 | */ 49 | function mint(uint mintAmount) override external returns (uint) { 50 | mintInternal(mintAmount); 51 | return NO_ERROR; 52 | } 53 | 54 | /** 55 | * @notice Sender redeems cTokens in exchange for the underlying asset 56 | * @dev Accrues interest whether or not the operation succeeds, unless reverted 57 | * @param redeemTokens The number of cTokens to redeem into underlying 58 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 59 | */ 60 | function redeem(uint redeemTokens) override external returns (uint) { 61 | redeemInternal(redeemTokens); 62 | return NO_ERROR; 63 | } 64 | 65 | /** 66 | * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset 67 | * @dev Accrues interest whether or not the operation succeeds, unless reverted 68 | * @param redeemAmount The amount of underlying to redeem 69 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 70 | */ 71 | function redeemUnderlying(uint redeemAmount) override external returns (uint) { 72 | redeemUnderlyingInternal(redeemAmount); 73 | return NO_ERROR; 74 | } 75 | 76 | /** 77 | * @notice Sender borrows assets from the protocol to their own address 78 | * @param borrowAmount The amount of the underlying asset to borrow 79 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 80 | */ 81 | function borrow(uint borrowAmount) override external returns (uint) { 82 | borrowInternal(borrowAmount); 83 | return NO_ERROR; 84 | } 85 | 86 | /** 87 | * @notice Sender repays their own borrow 88 | * @param repayAmount The amount to repay, or -1 for the full outstanding amount 89 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 90 | */ 91 | function repayBorrow(uint repayAmount) override external returns (uint) { 92 | repayBorrowInternal(repayAmount); 93 | return NO_ERROR; 94 | } 95 | 96 | /** 97 | * @notice Sender repays a borrow belonging to borrower 98 | * @param borrower the account with the debt being payed off 99 | * @param repayAmount The amount to repay, or -1 for the full outstanding amount 100 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 101 | */ 102 | function repayBorrowBehalf(address borrower, uint repayAmount) override external returns (uint) { 103 | repayBorrowBehalfInternal(borrower, repayAmount); 104 | return NO_ERROR; 105 | } 106 | 107 | /** 108 | * @notice The sender liquidates the borrowers collateral. 109 | * The collateral seized is transferred to the liquidator. 110 | * @param borrower The borrower of this cToken to be liquidated 111 | * @param repayAmount The amount of the underlying borrowed asset to repay 112 | * @param cTokenCollateral The market in which to seize collateral from the borrower 113 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 114 | */ 115 | function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) override external returns (uint) { 116 | liquidateBorrowInternal(borrower, repayAmount, cTokenCollateral); 117 | return NO_ERROR; 118 | } 119 | 120 | /** 121 | * @notice A public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to admin (timelock) 122 | * @param token The address of the ERC-20 token to sweep 123 | */ 124 | function sweepToken(EIP20NonStandardInterface token) override external { 125 | require(msg.sender == admin, "CErc20::sweepToken: only admin can sweep tokens"); 126 | require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"); 127 | uint256 balance = token.balanceOf(address(this)); 128 | token.transfer(admin, balance); 129 | } 130 | 131 | /** 132 | * @notice The sender adds to reserves. 133 | * @param addAmount The amount fo underlying token to add as reserves 134 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 135 | */ 136 | function _addReserves(uint addAmount) override external returns (uint) { 137 | return _addReservesInternal(addAmount); 138 | } 139 | 140 | /*** Safe Token ***/ 141 | 142 | /** 143 | * @notice Gets balance of this contract in terms of the underlying 144 | * @dev This excludes the value of the current message, if any 145 | * @return The quantity of underlying tokens owned by this contract 146 | */ 147 | function getCashPrior() virtual override internal view returns (uint) { 148 | EIP20Interface token = EIP20Interface(underlying); 149 | return token.balanceOf(address(this)); 150 | } 151 | 152 | /** 153 | * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case. 154 | * This will revert due to insufficient balance or insufficient allowance. 155 | * This function returns the actual amount received, 156 | * which may be less than `amount` if there is a fee attached to the transfer. 157 | * 158 | * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. 159 | * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca 160 | */ 161 | function doTransferIn(address from, uint amount) virtual override internal returns (uint) { 162 | // Read from storage once 163 | address underlying_ = underlying; 164 | EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying_); 165 | uint balanceBefore = EIP20Interface(underlying_).balanceOf(address(this)); 166 | token.transferFrom(from, address(this), amount); 167 | 168 | bool success; 169 | assembly { 170 | switch returndatasize() 171 | case 0 { // This is a non-standard ERC-20 172 | success := not(0) // set success to true 173 | } 174 | case 32 { // This is a compliant ERC-20 175 | returndatacopy(0, 0, 32) 176 | success := mload(0) // Set `success = returndata` of override external call 177 | } 178 | default { // This is an excessively non-compliant ERC-20, revert. 179 | revert(0, 0) 180 | } 181 | } 182 | require(success, "TOKEN_TRANSFER_IN_FAILED"); 183 | 184 | // Calculate the amount that was *actually* transferred 185 | uint balanceAfter = EIP20Interface(underlying_).balanceOf(address(this)); 186 | return balanceAfter - balanceBefore; // underflow already checked above, just subtract 187 | } 188 | 189 | /** 190 | * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory 191 | * error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to 192 | * insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified 193 | * it is >= amount, this should not revert in normal conditions. 194 | * 195 | * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. 196 | * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca 197 | */ 198 | function doTransferOut(address payable to, uint amount) virtual override internal { 199 | EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); 200 | token.transfer(to, amount); 201 | 202 | bool success; 203 | assembly { 204 | switch returndatasize() 205 | case 0 { // This is a non-standard ERC-20 206 | success := not(0) // set success to true 207 | } 208 | case 32 { // This is a compliant ERC-20 209 | returndatacopy(0, 0, 32) 210 | success := mload(0) // Set `success = returndata` of override external call 211 | } 212 | default { // This is an excessively non-compliant ERC-20, revert. 213 | revert(0, 0) 214 | } 215 | } 216 | require(success, "TOKEN_TRANSFER_OUT_FAILED"); 217 | } 218 | 219 | /** 220 | * @notice Admin call to delegate the votes of the COMP-like underlying 221 | * @param compLikeDelegatee The address to delegate votes to 222 | * @dev CTokens whose underlying are not CompLike should revert here 223 | */ 224 | function _delegateCompLikeTo(address compLikeDelegatee) external { 225 | require(msg.sender == admin, "only the admin may set the comp-like delegate"); 226 | CompLike(underlying).delegate(compLikeDelegatee); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /tests/ape/contracts/compound-v2/CTokenInterfaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.10; 3 | 4 | import "./ComptrollerInterface.sol"; 5 | import "./InterestRateModel.sol"; 6 | import "./EIP20NonStandardInterface.sol"; 7 | import "./ErrorReporter.sol"; 8 | 9 | contract CTokenStorage { 10 | /** 11 | * @dev Guard variable for re-entrancy checks 12 | */ 13 | bool internal _notEntered; 14 | 15 | /** 16 | * @notice EIP-20 token name for this token 17 | */ 18 | string public name; 19 | 20 | /** 21 | * @notice EIP-20 token symbol for this token 22 | */ 23 | string public symbol; 24 | 25 | /** 26 | * @notice EIP-20 token decimals for this token 27 | */ 28 | uint8 public decimals; 29 | 30 | // Maximum borrow rate that can ever be applied (.0005% / block) 31 | uint internal constant borrowRateMaxMantissa = 0.0005e16; 32 | 33 | // Maximum fraction of interest that can be set aside for reserves 34 | uint internal constant reserveFactorMaxMantissa = 1e18; 35 | 36 | /** 37 | * @notice Administrator for this contract 38 | */ 39 | address payable public admin; 40 | 41 | /** 42 | * @notice Pending administrator for this contract 43 | */ 44 | address payable public pendingAdmin; 45 | 46 | /** 47 | * @notice Contract which oversees inter-cToken operations 48 | */ 49 | ComptrollerInterface public comptroller; 50 | 51 | /** 52 | * @notice Model which tells what the current interest rate should be 53 | */ 54 | InterestRateModel public interestRateModel; 55 | 56 | // Initial exchange rate used when minting the first CTokens (used when totalSupply = 0) 57 | uint internal initialExchangeRateMantissa; 58 | 59 | /** 60 | * @notice Fraction of interest currently set aside for reserves 61 | */ 62 | uint public reserveFactorMantissa; 63 | 64 | /** 65 | * @notice Block number that interest was last accrued at 66 | */ 67 | uint public accrualBlockNumber; 68 | 69 | /** 70 | * @notice Accumulator of the total earned interest rate since the opening of the market 71 | */ 72 | uint public borrowIndex; 73 | 74 | /** 75 | * @notice Total amount of outstanding borrows of the underlying in this market 76 | */ 77 | uint public totalBorrows; 78 | 79 | /** 80 | * @notice Total amount of reserves of the underlying held in this market 81 | */ 82 | uint public totalReserves; 83 | 84 | /** 85 | * @notice Total number of tokens in circulation 86 | */ 87 | uint public totalSupply; 88 | 89 | // Official record of token balances for each account 90 | mapping (address => uint) internal accountTokens; 91 | 92 | // Approved token transfer amounts on behalf of others 93 | mapping (address => mapping (address => uint)) internal transferAllowances; 94 | 95 | /** 96 | * @notice Container for borrow balance information 97 | * @member principal Total balance (with accrued interest), after applying the most recent balance-changing action 98 | * @member interestIndex Global borrowIndex as of the most recent balance-changing action 99 | */ 100 | struct BorrowSnapshot { 101 | uint principal; 102 | uint interestIndex; 103 | } 104 | 105 | // Mapping of account addresses to outstanding borrow balances 106 | mapping(address => BorrowSnapshot) internal accountBorrows; 107 | 108 | /** 109 | * @notice Share of seized collateral that is added to reserves 110 | */ 111 | uint public constant protocolSeizeShareMantissa = 2.8e16; //2.8% 112 | } 113 | 114 | abstract contract CTokenInterface is CTokenStorage { 115 | /** 116 | * @notice Indicator that this is a CToken contract (for inspection) 117 | */ 118 | bool public constant isCToken = true; 119 | 120 | 121 | /*** Market Events ***/ 122 | 123 | /** 124 | * @notice Event emitted when interest is accrued 125 | */ 126 | event AccrueInterest(uint cashPrior, uint interestAccumulated, uint borrowIndex, uint totalBorrows); 127 | 128 | /** 129 | * @notice Event emitted when tokens are minted 130 | */ 131 | event Mint(address minter, uint mintAmount, uint mintTokens); 132 | 133 | /** 134 | * @notice Event emitted when tokens are redeemed 135 | */ 136 | event Redeem(address redeemer, uint redeemAmount, uint redeemTokens); 137 | 138 | /** 139 | * @notice Event emitted when underlying is borrowed 140 | */ 141 | event Borrow(address borrower, uint borrowAmount, uint accountBorrows, uint totalBorrows); 142 | 143 | /** 144 | * @notice Event emitted when a borrow is repaid 145 | */ 146 | event RepayBorrow(address payer, address borrower, uint repayAmount, uint accountBorrows, uint totalBorrows); 147 | 148 | /** 149 | * @notice Event emitted when a borrow is liquidated 150 | */ 151 | event LiquidateBorrow(address liquidator, address borrower, uint repayAmount, address cTokenCollateral, uint seizeTokens); 152 | 153 | 154 | /*** Admin Events ***/ 155 | 156 | /** 157 | * @notice Event emitted when pendingAdmin is changed 158 | */ 159 | event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); 160 | 161 | /** 162 | * @notice Event emitted when pendingAdmin is accepted, which means admin is updated 163 | */ 164 | event NewAdmin(address oldAdmin, address newAdmin); 165 | 166 | /** 167 | * @notice Event emitted when comptroller is changed 168 | */ 169 | event NewComptroller(ComptrollerInterface oldComptroller, ComptrollerInterface newComptroller); 170 | 171 | /** 172 | * @notice Event emitted when interestRateModel is changed 173 | */ 174 | event NewMarketInterestRateModel(InterestRateModel oldInterestRateModel, InterestRateModel newInterestRateModel); 175 | 176 | /** 177 | * @notice Event emitted when the reserve factor is changed 178 | */ 179 | event NewReserveFactor(uint oldReserveFactorMantissa, uint newReserveFactorMantissa); 180 | 181 | /** 182 | * @notice Event emitted when the reserves are added 183 | */ 184 | event ReservesAdded(address benefactor, uint addAmount, uint newTotalReserves); 185 | 186 | /** 187 | * @notice Event emitted when the reserves are reduced 188 | */ 189 | event ReservesReduced(address admin, uint reduceAmount, uint newTotalReserves); 190 | 191 | /** 192 | * @notice EIP20 Transfer event 193 | */ 194 | event Transfer(address indexed from, address indexed to, uint amount); 195 | 196 | /** 197 | * @notice EIP20 Approval event 198 | */ 199 | event Approval(address indexed owner, address indexed spender, uint amount); 200 | 201 | 202 | /*** User Interface ***/ 203 | 204 | function transfer(address dst, uint amount) virtual external returns (bool); 205 | function transferFrom(address src, address dst, uint amount) virtual external returns (bool); 206 | function approve(address spender, uint amount) virtual external returns (bool); 207 | function allowance(address owner, address spender) virtual external view returns (uint); 208 | function balanceOf(address owner) virtual external view returns (uint); 209 | function balanceOfUnderlying(address owner) virtual external returns (uint); 210 | function getAccountSnapshot(address account) virtual external view returns (uint, uint, uint, uint); 211 | function borrowRatePerBlock() virtual external view returns (uint); 212 | function supplyRatePerBlock() virtual external view returns (uint); 213 | function totalBorrowsCurrent() virtual external returns (uint); 214 | function borrowBalanceCurrent(address account) virtual external returns (uint); 215 | function borrowBalanceStored(address account) virtual external view returns (uint); 216 | function exchangeRateCurrent() virtual external returns (uint); 217 | function exchangeRateStored() virtual external view returns (uint); 218 | function getCash() virtual external view returns (uint); 219 | function accrueInterest() virtual external returns (uint); 220 | function seize(address liquidator, address borrower, uint seizeTokens) virtual external returns (uint); 221 | 222 | 223 | /*** Admin Functions ***/ 224 | 225 | function _setPendingAdmin(address payable newPendingAdmin) virtual external returns (uint); 226 | function _acceptAdmin() virtual external returns (uint); 227 | function _setComptroller(ComptrollerInterface newComptroller) virtual external returns (uint); 228 | function _setReserveFactor(uint newReserveFactorMantissa) virtual external returns (uint); 229 | function _reduceReserves(uint reduceAmount) virtual external returns (uint); 230 | function _setInterestRateModel(InterestRateModel newInterestRateModel) virtual external returns (uint); 231 | } 232 | 233 | contract CErc20Storage { 234 | /** 235 | * @notice Underlying asset for this CToken 236 | */ 237 | address public underlying; 238 | } 239 | 240 | abstract contract CErc20Interface is CErc20Storage { 241 | 242 | /*** User Interface ***/ 243 | 244 | function mint(uint mintAmount) virtual external returns (uint); 245 | function redeem(uint redeemTokens) virtual external returns (uint); 246 | function redeemUnderlying(uint redeemAmount) virtual external returns (uint); 247 | function borrow(uint borrowAmount) virtual external returns (uint); 248 | function repayBorrow(uint repayAmount) virtual external returns (uint); 249 | function repayBorrowBehalf(address borrower, uint repayAmount) virtual external returns (uint); 250 | function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) virtual external returns (uint); 251 | function sweepToken(EIP20NonStandardInterface token) virtual external; 252 | 253 | 254 | /*** Admin Functions ***/ 255 | 256 | function _addReserves(uint addAmount) virtual external returns (uint); 257 | } 258 | 259 | contract CDelegationStorage { 260 | /** 261 | * @notice Implementation address for this contract 262 | */ 263 | address public implementation; 264 | } 265 | 266 | abstract contract CDelegatorInterface is CDelegationStorage { 267 | /** 268 | * @notice Emitted when implementation is changed 269 | */ 270 | event NewImplementation(address oldImplementation, address newImplementation); 271 | 272 | /** 273 | * @notice Called by the admin to update the implementation of the delegator 274 | * @param implementation_ The address of the new implementation for delegation 275 | * @param allowResign Flag to indicate whether to call _resignImplementation on the old implementation 276 | * @param becomeImplementationData The encoded bytes data to be passed to _becomeImplementation 277 | */ 278 | function _setImplementation(address implementation_, bool allowResign, bytes memory becomeImplementationData) virtual external; 279 | } 280 | 281 | abstract contract CDelegateInterface is CDelegationStorage { 282 | /** 283 | * @notice Called by the delegator on a delegate to initialize it for duty 284 | * @dev Should revert if any issues arise which make it unfit for delegation 285 | * @param data The encoded bytes data for any initialization 286 | */ 287 | function _becomeImplementation(bytes memory data) virtual external; 288 | 289 | /** 290 | * @notice Called by the delegator on a delegate to forfeit its responsibility 291 | */ 292 | function _resignImplementation() virtual external; 293 | } 294 | -------------------------------------------------------------------------------- /tests/web3client/test_rpc_log_middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from pathlib import Path 4 | 5 | import pytest 6 | from pytest import LogCaptureFixture 7 | from web3 import Web3 8 | 9 | import ape 10 | from web3client.base_client import BaseClient 11 | from web3client.erc20_client import Erc20Client 12 | from web3client.middlewares.rpc_log_middleware import ( 13 | MemoryLog, 14 | PythonLog, 15 | RequestEntry, 16 | ResponseEntry, 17 | RPCEndpoint, 18 | RPCResponse, 19 | construct_generic_rpc_log_middleware, 20 | tx_rpc_log_middleware, 21 | ) 22 | 23 | 24 | @pytest.fixture 25 | def request_entry() -> RequestEntry: 26 | return RequestEntry( 27 | id="whatever", 28 | method=RPCEndpoint("test_method"), 29 | params={"param1": "value1", "param2": "value2"}, 30 | timestamp=datetime.now(), 31 | type="request", 32 | w3=Web3(), 33 | ) 34 | 35 | 36 | @pytest.fixture 37 | def response_entry() -> ResponseEntry: 38 | return ResponseEntry( 39 | id="whatever", 40 | method=RPCEndpoint("test_method"), 41 | params={"param1": "value1", "param2": "value2"}, 42 | response=RPCResponse(result="test_result"), 43 | timestamp=datetime.now(), 44 | type="response", 45 | elapsed=10, 46 | w3=Web3(), 47 | ) 48 | 49 | 50 | @pytest.mark.local 51 | def test_rpc_log_middleware_memory_log_send( 52 | alice_base_client: BaseClient, 53 | bob: ape.api.AccountAPI, 54 | ) -> None: 55 | """Send ETH and check that the internal log contains the request and 56 | response""" 57 | log = MemoryLog(rpc_whitelist=["eth_sendRawTransaction"]) 58 | alice_base_client.w3.middleware_onion.add(construct_generic_rpc_log_middleware(log)) 59 | tx_hash = alice_base_client.send_eth_in_wei(bob.address, 10**18) 60 | assert len(log.entries) == 2 61 | # Extract log entries 62 | logged_requests = log.get_requests() 63 | logged_responses = log.get_responses() 64 | assert len(logged_requests) == 1 65 | assert len(logged_responses) == 1 66 | logged_request = logged_requests[0] 67 | logged_response = logged_responses[0] 68 | # Check first log entry - the request 69 | assert logged_request.type == "request" 70 | assert logged_request.method == "eth_sendRawTransaction" 71 | assert type(logged_request.params[0]) is str 72 | assert logged_request.params[0].startswith("0x") 73 | assert isinstance(logged_request.timestamp, datetime) 74 | # Check second log entry - the response 75 | assert logged_response.type == "response" 76 | assert logged_response.method == log.entries[0].method 77 | assert logged_response.params == log.entries[0].params 78 | assert logged_response.timestamp > log.entries[0].timestamp 79 | assert logged_response.response["result"] == tx_hash 80 | assert logged_response.elapsed > 0 81 | 82 | 83 | @pytest.mark.local 84 | def test_rpc_log_middleware_memory_log_estimate_and_send( 85 | alice_base_client: BaseClient, 86 | bob: ape.api.AccountAPI, 87 | ) -> None: 88 | """Send ETH and check that the internal log contains the request and 89 | response, for both the eth_estimateGas and eth_sendRawTransaction methods""" 90 | log = MemoryLog(rpc_whitelist=["eth_estimateGas", "eth_sendRawTransaction"]) 91 | alice_base_client.w3.middleware_onion.add(construct_generic_rpc_log_middleware(log)) 92 | tx_hash = alice_base_client.send_eth_in_wei(bob.address, 10**18) 93 | assert len(log.entries) == 4 94 | # Extract log entries 95 | requests = log.get_requests() 96 | responses = log.get_responses() 97 | # Estimate gas request 98 | assert requests[0].method == "eth_estimateGas" 99 | assert type(requests[0].params[0]) is dict 100 | assert requests[0].params[0].keys() == {"from", "to", "value"} 101 | # Estimate gas response 102 | assert int(responses[0].response["result"], 16) > 0 103 | # Send raw transaction request 104 | assert requests[1].method == "eth_sendRawTransaction" 105 | # Send raw transaction response 106 | assert responses[1].response["result"] == tx_hash 107 | 108 | 109 | @pytest.mark.local 110 | def test_rpc_log_middleware_memory_log_empty_whitelist( 111 | alice_base_client: BaseClient, 112 | bob: ape.api.AccountAPI, 113 | ) -> None: 114 | """Send ETH and check that the internal log is empty if the whitelist is 115 | None""" 116 | log = MemoryLog(rpc_whitelist=[]) 117 | alice_base_client.w3.middleware_onion.add(construct_generic_rpc_log_middleware(log)) 118 | alice_base_client.send_eth_in_wei(bob.address, 10**18) 119 | assert len(log.entries) == 0 120 | 121 | 122 | @pytest.mark.local 123 | def test_rpc_log_middleware_memory_log_parse_raw( 124 | alice_erc20_client: Erc20Client, 125 | bob: ape.api.AccountAPI, 126 | ) -> None: 127 | """Send an ETH transfer and a token transfer, and check that the requests 128 | are included in the log entries with their decoded tx_data""" 129 | log = MemoryLog(rpc_whitelist=["eth_sendRawTransaction"]) 130 | alice_erc20_client.w3.middleware_onion.add( 131 | construct_generic_rpc_log_middleware(log) 132 | ) 133 | # Send ETH transfer. This will trigger an eth_estimateGas request 134 | tx_hash_eth = alice_erc20_client.send_eth_in_wei(bob.address, 10**18) 135 | # Send a token transfer. This will trigger an eth_estimateGas request 136 | tx_hash_erc20 = alice_erc20_client.transfer(bob.address, 10**18) 137 | # Check that the tx was logged 138 | tx_requests = log.get_tx_requests() 139 | assert len(tx_requests) == 2 140 | # Check decoding for the ETH transfer 141 | tx_data_eth = tx_requests[0].parsed_tx_data 142 | assert Web3.to_hex(tx_data_eth["hash"]) == tx_hash_eth 143 | assert tx_data_eth["from"] == alice_erc20_client.user_address 144 | assert tx_data_eth["to"] == bob.address 145 | assert int(tx_data_eth["value"]) == 10**18 146 | # Check decoding for the token transfer 147 | tx_data_erc20 = tx_requests[1].parsed_tx_data 148 | assert Web3.to_hex(tx_data_erc20["hash"]) == tx_hash_erc20 149 | assert tx_data_erc20["from"] == alice_erc20_client.user_address 150 | assert tx_data_erc20["to"] == alice_erc20_client.contract_address 151 | assert int(tx_data_erc20["value"]) == 0 152 | # Check that tx response does not contain tx_data or tx_receipt 153 | # because we did not request it 154 | tx_responses = log.get_tx_responses() 155 | assert len(tx_responses) == 2 156 | # ... ETH transfer 157 | assert tx_responses[0].response["result"] == tx_hash_eth 158 | assert tx_responses[0].tx_data == None 159 | assert tx_responses[0].tx_receipt == None 160 | # ... token transfer 161 | assert tx_responses[1].response["result"] == tx_hash_erc20 162 | assert tx_responses[1].tx_data == None 163 | assert tx_responses[1].tx_receipt == None 164 | 165 | 166 | @pytest.mark.local 167 | def test_rpc_log_middleware_memory_log_fetch( 168 | alice_base_client: BaseClient, 169 | bob: ape.api.AccountAPI, 170 | ) -> None: 171 | """Send ETH and check that the transaction response is included in the 172 | entries with its fetched tx_data and tx_receipts""" 173 | log = MemoryLog( 174 | rpc_whitelist=["eth_sendRawTransaction"], 175 | fetch_tx_data=True, 176 | fetch_tx_receipt=True, 177 | ) 178 | alice_base_client.w3.middleware_onion.add(construct_generic_rpc_log_middleware(log)) 179 | tx_hash = alice_base_client.send_eth_in_wei(bob.address, 10**18) 180 | # Check tx response 181 | tx_responses = log.get_tx_responses() 182 | assert len(tx_responses) == 1 183 | # Check response tx_data 184 | tx_data = tx_responses[0].tx_data 185 | assert int(tx_data["value"]) == 10**18 186 | # Check response tx_receipt 187 | tx_receipt = tx_responses[0].tx_receipt 188 | assert Web3.to_hex(tx_receipt["transactionHash"]) == tx_hash 189 | assert type(tx_receipt["gasUsed"]) is int 190 | 191 | 192 | @pytest.mark.local 193 | def test_rpc_log_middleware_tx_rpc_log_middleware( 194 | alice_base_client: BaseClient, bob: ape.api.AccountAPI, tmp_path: Path 195 | ) -> None: 196 | # Set logger so that it prints to file all INFO messages 197 | file = tmp_path / "rpc.log" 198 | logger = logging.getLogger("web3client.RpcLog") 199 | logger.setLevel(logging.INFO) 200 | fh = logging.FileHandler(file.resolve()) 201 | fh.setFormatter(logging.Formatter("%(asctime)s %(message)s")) 202 | logger.addHandler(fh) 203 | # Add tx_rpc_log_middleware to the client 204 | alice_base_client.w3.middleware_onion.add(tx_rpc_log_middleware) 205 | # Send ETH 206 | alice_base_client.send_eth_in_wei(bob.address, 10**18) 207 | # Check that the log file contains the request and response 208 | assert file.exists(), f"file '{file}' not found" 209 | content = file.read_text() 210 | assert "[REQ " in content 211 | assert "[RES " in content 212 | 213 | 214 | # _ _ _ _ 215 | # | | | | _ _ (_) | |_ 216 | # | |_| | | ' \ | | | _| 217 | # \___/ |_||_| |_| \__| 218 | 219 | 220 | def test_unit_rpc_log_middleware_memory_log( 221 | request_entry: RequestEntry, 222 | response_entry: ResponseEntry, 223 | ) -> None: 224 | """Unit test for the ``MemoryLog`` class""" 225 | # Arrange 226 | 227 | # Act 228 | memory_log = MemoryLog() 229 | memory_log.handle_request( 230 | id=request_entry.id, 231 | method=RPCEndpoint(request_entry.method), 232 | params=request_entry.params, 233 | w3=request_entry.w3, 234 | ) 235 | memory_log.handle_response( 236 | id=response_entry.id, 237 | method=RPCEndpoint(response_entry.method), 238 | params=response_entry.params, 239 | w3=response_entry.w3, 240 | response=response_entry.response, 241 | elapsed=response_entry.elapsed, 242 | ) 243 | 244 | # Assert 245 | assert len(memory_log.entries) == 2 246 | # ... request entry 247 | logged_request = memory_log.get_requests()[0] 248 | assert logged_request.id == request_entry.id 249 | assert logged_request.method == request_entry.method 250 | assert logged_request.params == request_entry.params 251 | assert logged_request.timestamp > request_entry.timestamp 252 | assert logged_request.type == request_entry.type 253 | # ... response entry 254 | logged_response = memory_log.get_responses()[0] 255 | assert logged_response.id == response_entry.id 256 | assert logged_response.method == response_entry.method 257 | assert logged_response.params == response_entry.params 258 | assert logged_response.response == response_entry.response 259 | assert logged_response.timestamp > response_entry.timestamp 260 | assert logged_response.type == response_entry.type 261 | 262 | 263 | def test_unit_rpc_log_middleware_python_log( 264 | caplog: LogCaptureFixture, 265 | request_entry: RequestEntry, 266 | response_entry: ResponseEntry, 267 | ) -> None: 268 | """Unit test for the ``PythonLog`` class""" 269 | # Arrange 270 | entry = { 271 | "id": "whatever", 272 | "method": RPCEndpoint("test_method"), 273 | "params": {"param1": "value1", "param2": "value2"}, 274 | "timestamp": datetime.now(), 275 | "type": "request", 276 | "w3": Web3(), 277 | } 278 | logger = logging.getLogger("test_logger") 279 | logger.setLevel(logging.INFO) 280 | 281 | # Act 282 | python_log = PythonLog(logger=logger) 283 | python_log.log_request(request_entry) 284 | python_log.log_response(response_entry) 285 | 286 | # Get all info records in `test_logger` log 287 | records = [ 288 | r 289 | for r in caplog.record_tuples 290 | if r[0] == "test_logger" and r[1] == logging.INFO 291 | ] 292 | 293 | # Assert 294 | assert len(records) == 2 295 | assert "[REQ" in records[0][2] 296 | assert "[RES" in records[1][2] 297 | -------------------------------------------------------------------------------- /src/web3client/abi/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "address", 13 | "name": "owner", 14 | "type": "address" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "spender", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "value", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "Approval", 30 | "type": "event" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { 36 | "indexed": true, 37 | "internalType": "address", 38 | "name": "previousOwner", 39 | "type": "address" 40 | }, 41 | { 42 | "indexed": true, 43 | "internalType": "address", 44 | "name": "newOwner", 45 | "type": "address" 46 | } 47 | ], 48 | "name": "OwnershipTransferred", 49 | "type": "event" 50 | }, 51 | { 52 | "anonymous": false, 53 | "inputs": [ 54 | { 55 | "indexed": false, 56 | "internalType": "address", 57 | "name": "account", 58 | "type": "address" 59 | } 60 | ], 61 | "name": "Paused", 62 | "type": "event" 63 | }, 64 | { 65 | "anonymous": false, 66 | "inputs": [ 67 | { 68 | "indexed": true, 69 | "internalType": "address", 70 | "name": "addr", 71 | "type": "address" 72 | }, 73 | { 74 | "indexed": false, 75 | "internalType": "bool", 76 | "name": "status", 77 | "type": "bool" 78 | } 79 | ], 80 | "name": "SetMinter", 81 | "type": "event" 82 | }, 83 | { 84 | "anonymous": false, 85 | "inputs": [ 86 | { 87 | "indexed": true, 88 | "internalType": "address", 89 | "name": "from", 90 | "type": "address" 91 | }, 92 | { 93 | "indexed": true, 94 | "internalType": "address", 95 | "name": "to", 96 | "type": "address" 97 | }, 98 | { 99 | "indexed": false, 100 | "internalType": "uint256", 101 | "name": "value", 102 | "type": "uint256" 103 | } 104 | ], 105 | "name": "Transfer", 106 | "type": "event" 107 | }, 108 | { 109 | "anonymous": false, 110 | "inputs": [ 111 | { 112 | "indexed": false, 113 | "internalType": "address", 114 | "name": "account", 115 | "type": "address" 116 | } 117 | ], 118 | "name": "Unpaused", 119 | "type": "event" 120 | }, 121 | { 122 | "inputs": [ 123 | { 124 | "internalType": "address", 125 | "name": "owner", 126 | "type": "address" 127 | }, 128 | { 129 | "internalType": "address", 130 | "name": "spender", 131 | "type": "address" 132 | } 133 | ], 134 | "name": "allowance", 135 | "outputs": [ 136 | { 137 | "internalType": "uint256", 138 | "name": "", 139 | "type": "uint256" 140 | } 141 | ], 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [ 147 | { 148 | "internalType": "address", 149 | "name": "spender", 150 | "type": "address" 151 | }, 152 | { 153 | "internalType": "uint256", 154 | "name": "amount", 155 | "type": "uint256" 156 | } 157 | ], 158 | "name": "approve", 159 | "outputs": [ 160 | { 161 | "internalType": "bool", 162 | "name": "", 163 | "type": "bool" 164 | } 165 | ], 166 | "stateMutability": "nonpayable", 167 | "type": "function" 168 | }, 169 | { 170 | "inputs": [ 171 | { 172 | "internalType": "address", 173 | "name": "account", 174 | "type": "address" 175 | } 176 | ], 177 | "name": "balanceOf", 178 | "outputs": [ 179 | { 180 | "internalType": "uint256", 181 | "name": "", 182 | "type": "uint256" 183 | } 184 | ], 185 | "stateMutability": "view", 186 | "type": "function" 187 | }, 188 | { 189 | "inputs": [ 190 | { 191 | "internalType": "uint256", 192 | "name": "amount", 193 | "type": "uint256" 194 | } 195 | ], 196 | "name": "burn", 197 | "outputs": [], 198 | "stateMutability": "nonpayable", 199 | "type": "function" 200 | }, 201 | { 202 | "inputs": [], 203 | "name": "decimals", 204 | "outputs": [ 205 | { 206 | "internalType": "uint8", 207 | "name": "", 208 | "type": "uint8" 209 | } 210 | ], 211 | "stateMutability": "view", 212 | "type": "function" 213 | }, 214 | { 215 | "inputs": [ 216 | { 217 | "internalType": "address", 218 | "name": "spender", 219 | "type": "address" 220 | }, 221 | { 222 | "internalType": "uint256", 223 | "name": "subtractedValue", 224 | "type": "uint256" 225 | } 226 | ], 227 | "name": "decreaseAllowance", 228 | "outputs": [ 229 | { 230 | "internalType": "bool", 231 | "name": "", 232 | "type": "bool" 233 | } 234 | ], 235 | "stateMutability": "nonpayable", 236 | "type": "function" 237 | }, 238 | { 239 | "inputs": [ 240 | { 241 | "internalType": "address", 242 | "name": "spender", 243 | "type": "address" 244 | }, 245 | { 246 | "internalType": "uint256", 247 | "name": "addedValue", 248 | "type": "uint256" 249 | } 250 | ], 251 | "name": "increaseAllowance", 252 | "outputs": [ 253 | { 254 | "internalType": "bool", 255 | "name": "", 256 | "type": "bool" 257 | } 258 | ], 259 | "stateMutability": "nonpayable", 260 | "type": "function" 261 | }, 262 | { 263 | "inputs": [ 264 | { 265 | "internalType": "address", 266 | "name": "_to", 267 | "type": "address" 268 | }, 269 | { 270 | "internalType": "uint256", 271 | "name": "_amount", 272 | "type": "uint256" 273 | } 274 | ], 275 | "name": "mint", 276 | "outputs": [], 277 | "stateMutability": "nonpayable", 278 | "type": "function" 279 | }, 280 | { 281 | "inputs": [ 282 | { 283 | "internalType": "address", 284 | "name": "", 285 | "type": "address" 286 | } 287 | ], 288 | "name": "minters", 289 | "outputs": [ 290 | { 291 | "internalType": "bool", 292 | "name": "", 293 | "type": "bool" 294 | } 295 | ], 296 | "stateMutability": "view", 297 | "type": "function" 298 | }, 299 | { 300 | "inputs": [], 301 | "name": "name", 302 | "outputs": [ 303 | { 304 | "internalType": "string", 305 | "name": "", 306 | "type": "string" 307 | } 308 | ], 309 | "stateMutability": "view", 310 | "type": "function" 311 | }, 312 | { 313 | "inputs": [], 314 | "name": "owner", 315 | "outputs": [ 316 | { 317 | "internalType": "address", 318 | "name": "", 319 | "type": "address" 320 | } 321 | ], 322 | "stateMutability": "view", 323 | "type": "function" 324 | }, 325 | { 326 | "inputs": [], 327 | "name": "pause", 328 | "outputs": [], 329 | "stateMutability": "nonpayable", 330 | "type": "function" 331 | }, 332 | { 333 | "inputs": [], 334 | "name": "paused", 335 | "outputs": [ 336 | { 337 | "internalType": "bool", 338 | "name": "", 339 | "type": "bool" 340 | } 341 | ], 342 | "stateMutability": "view", 343 | "type": "function" 344 | }, 345 | { 346 | "inputs": [], 347 | "name": "renounceOwnership", 348 | "outputs": [], 349 | "stateMutability": "nonpayable", 350 | "type": "function" 351 | }, 352 | { 353 | "inputs": [ 354 | { 355 | "internalType": "address", 356 | "name": "addr", 357 | "type": "address" 358 | }, 359 | { 360 | "internalType": "bool", 361 | "name": "status", 362 | "type": "bool" 363 | } 364 | ], 365 | "name": "setMinter", 366 | "outputs": [], 367 | "stateMutability": "nonpayable", 368 | "type": "function" 369 | }, 370 | { 371 | "inputs": [], 372 | "name": "symbol", 373 | "outputs": [ 374 | { 375 | "internalType": "string", 376 | "name": "", 377 | "type": "string" 378 | } 379 | ], 380 | "stateMutability": "view", 381 | "type": "function" 382 | }, 383 | { 384 | "inputs": [], 385 | "name": "totalSupply", 386 | "outputs": [ 387 | { 388 | "internalType": "uint256", 389 | "name": "", 390 | "type": "uint256" 391 | } 392 | ], 393 | "stateMutability": "view", 394 | "type": "function" 395 | }, 396 | { 397 | "inputs": [ 398 | { 399 | "internalType": "address", 400 | "name": "recipient", 401 | "type": "address" 402 | }, 403 | { 404 | "internalType": "uint256", 405 | "name": "amount", 406 | "type": "uint256" 407 | } 408 | ], 409 | "name": "transfer", 410 | "outputs": [ 411 | { 412 | "internalType": "bool", 413 | "name": "", 414 | "type": "bool" 415 | } 416 | ], 417 | "stateMutability": "nonpayable", 418 | "type": "function" 419 | }, 420 | { 421 | "inputs": [ 422 | { 423 | "internalType": "address", 424 | "name": "sender", 425 | "type": "address" 426 | }, 427 | { 428 | "internalType": "address", 429 | "name": "recipient", 430 | "type": "address" 431 | }, 432 | { 433 | "internalType": "uint256", 434 | "name": "amount", 435 | "type": "uint256" 436 | } 437 | ], 438 | "name": "transferFrom", 439 | "outputs": [ 440 | { 441 | "internalType": "bool", 442 | "name": "", 443 | "type": "bool" 444 | } 445 | ], 446 | "stateMutability": "nonpayable", 447 | "type": "function" 448 | }, 449 | { 450 | "inputs": [ 451 | { 452 | "internalType": "address", 453 | "name": "newOwner", 454 | "type": "address" 455 | } 456 | ], 457 | "name": "transferOwnership", 458 | "outputs": [], 459 | "stateMutability": "nonpayable", 460 | "type": "function" 461 | }, 462 | { 463 | "inputs": [], 464 | "name": "unpause", 465 | "outputs": [], 466 | "stateMutability": "nonpayable", 467 | "type": "function" 468 | } 469 | ] --------------------------------------------------------------------------------