ERC-4626 deposit and redeem
Here is a Python example how to deposit and redeem ERC-4626 vaults.
This is a script that performs deposit and redeem operations on an ERC-4626 vault
It can be run in simulation mode (Anvil mainnet fork)
The chain id and vault are given as a command line arguments
The script it is multichain: it will automatically pick JSON-RPC connection for the given chain id
Currently only USDC deposits supported
Then to run this script:
# Test Harvest finance USDC Autopilot vault on IPOR on Base mainnet fork
export JSON_RPC_BASE=...
python scripts/erc-4626/erc-4626-deposit-redeem.py \
--simulate \
--vault 8453,0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4
Output looks like:
Created provider lb.drpc.org, using request args {'headers': {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}, 'timeout': (3.0, 30.0)}, headers {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}
Created provider base-mainnet.g.alchemy.com, using request args {'headers': {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}, 'timeout': (3.0, 30.0)}, headers {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}
Configuring MultiProviderWeb3. Call providers: ['lb.drpc.org', 'base-mainnet.g.alchemy.com'], transact providers -
Using JSON RPC provider fallbacks lb.drpc.org, base-mainnet.g.alchemy.com for chain Base
Forking Base with Anvil
Attempting to allocate port 27388 to Anvil
Multi RPC detected, using Anvil at the first RPC endpoint https://lb.drpc.org/ogrpc?network=base&dkey=AiWA4TvYpkijvapnvFlyx_UuJsZmMjkR8JUBzoXPVSjK
Launching anvil: anvil --port 27388 --fork-url https://lb.drpc.org/ogrpc?network=base&dkey=AiWA4TvYpkijvapnvFlyx_UuJsZmMjkR8JUBzoXPVSjK --hardfork cancun --code-size-limit 99999
anvil forked network 8453, the current block is 31,815,357, Anvil JSON-RPC is http://localhost:27388
Making request with data: <class 'web3.providers.rpc.HTTPProvider'> {'headers': {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}, 'timeout': 3.0}
Created provider localhost:27388, using request args {'headers': {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}, 'timeout': (10.0, 60.0)}, headers {'Content-Type': 'application/json', 'User-Agent': "web3.py/6.14.0/<class 'web3.providers.rpc.HTTPProvider'>"}
Configuring MultiProviderWeb3. Call providers: ['localhost:27388'], transact providers -
Synced nonce for 0x1A76D579415532C527485FC83FDBc954F9b67cE6 to 0
Creating a simulated wallet 0x1A76D579415532C527485FC83FDBc954F9b67cE6 with USDC and ETH funding for testing
Will not retry, method eth_call, as not a retryable exception <class 'ValueError'>: {'code': 3, 'message': 'execution reverted: custom error 0x1425ea42', 'data': '0x1425ea42'}
Using vault Autopilot USDC Base (0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4)
Gas balance: 11.0 ETH
USDC balance: 15598593.712583
Depositing 10.00 USDC to vault 0x0d877dc7c8fa3ad980dfdb18b48ec9f8768359c4
Depositing...
Depositing to vault 0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4, amount 10.00, from 0x1A76D579415532C527485FC83FDBc954F9b67cE6
Broadcasting transaction approve(): 0x03d57ddb1cc2984a137565c1597227cc147844ba09bc220189e0fc4fdd591a01
Broadcasting transaction deposit(): 0xc297da0c345a41b2586229cba3fefb1c37a4663cd266279aa3f8beb51cfc99e9
We received 9.775728 bAutopilot_USDC
Redeeming, simulated waiting for 1800 seconds
Redeeming from vault 0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4, amount 9.77572796, from 0x1A76D579415532C527485FC83FDBc954F9b67cE6
Broadcasting transaction approve(): 0x0c83593eec26c4c1dea4a8f4b7ceeb742d5be4bdd5f351d5d07e324590127672
Broadcasting transaction redeem(): 0x35973ecdf96ad79393a365cd6550ee9ccf6025af696b79e3bc1b95055fe38355
Deposit value: 10.00 USDC
Redeem value: 9.999998 USDC
Share count: 9.77572796 bAutopilot_USDC
Slippage: -0.0000%
All done
"""ERC-4626 vault deposit and redeem script.
- This is a script that performs deposit and redeem operations on an ERC-4626 vault
- It can be run in simulation mode (:ref:`Anvil` mainnet fork)
- The chain id and vault are given as a command line arguments
- The script it is multichain: it will automatically pick JSON-RPC connection
for the given chain id
- Currently only USDC deposits supported
To run:
.. code-block:: shell
# Test Harvest finance USDC Autopilot vault on IPOR on Base mainnet fork.
# You need your own Base JSON-RPC provider.
export JSON_RPC_BASE=...
python scripts/erc-4626/erc-4626-deposit-redeem.py \
--simulate \
--vault 8453,0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4
Another example using Spark USDC vault on Base mainnet fork:
.. code-block:: shell
# Test Harvest finance USDC Autopilot vault on IPOR on Base mainnet fork.
# You need your own Base JSON-RPC provider.
export JSON_RPC_BASE=...
python scripts/erc-4626/erc-4626-deposit-redeem.py \
--simulate \
--vault 8453,0x7bfa7c4f149e7415b73bdedfe609237e29cbf34a
"""
import logging
import os
import datetime
from decimal import Decimal
import argparse
from typing import cast
from web3 import Web3
from web3.contract.contract import ContractFunction
from eth_defi.chain import get_chain_name, get_block_time
from eth_defi.erc_4626.classification import create_vault_instance, detect_vault_features
from eth_defi.erc_4626.flow import approve_and_deposit_4626, approve_and_redeem_4626
from eth_defi.erc_4626.vault import ERC4626Vault
from eth_defi.hotwallet import HotWallet
from eth_defi.provider.anvil import fork_network_anvil, create_fork_funded_wallet, is_anvil, mine
from eth_defi.provider.env import read_json_rpc_url
from eth_defi.provider.multi_provider import create_multi_provider_web3
from eth_defi.provider.named import get_provider_name
from eth_defi.timestamp import get_block_timestamp
from eth_defi.token import fetch_erc20_details, USDC_NATIVE_TOKEN, LARGE_USDC_HOLDERS
from eth_defi.trace import assert_transaction_success_with_explanation
from eth_defi.tx import get_tx_broadcast_data
from eth_defi.utils import setup_console_logging
from eth_defi.vault.base import VaultSpec, VaultBase
logger = logging.getLogger(__name__)
def parse_args():
parser = argparse.ArgumentParser(description="ERC-4626 vault deposit and redeem script.")
parser.add_argument("--vault", type=str, required=True, help="Vault chain id and contract address. E.g. 123,12312312")
parser.add_argument("--usdc-forked-wallet", type=str, required=False, help="For simulationm, onchain address holding balance for USDC which we are going to use")
parser.add_argument("--json-rpc-url", type=str, required=False, help="Give JSON-RPC URL - otherwise picked from environment variables")
parser.add_argument("--simulate", action="store_true", help="Run in simulation mode by doing Anvil work")
parser.add_argument("--fork-block-number", type=int, required=False, help="Fork the mainnet at a specific block number (example: 341830407)")
parser.add_argument("--private-key", type=str, required=False, help="Private key for the hot wallet when not simulated, must start 0x")
parser.add_argument("--deposit-value", type=str, required=False, help="Test deposit value in USDC, e.g. 1000.0", default="10.00")
parser.add_argument("--simplified-logging", action="store_true", help="Use simplified output without timestamps")
parser.add_argument("--redeem-wait-seconds", type=float, required=False, help="How many blocks to mine for redeem timelock on the vault", default=1800)
return parser.parse_args()
def deposit_redeem(
web3: Web3,
vault: VaultBase,
hot_wallet: HotWallet,
deposit_value: Decimal,
redeem_wait_seconds: float | None,
) -> dict[str, Decimal]:
"""Perform deposit and redeem transactions.
- 4 transactions total with approves
- `See here for IPOR error codes <https://www.codeslaw.app/contracts/base/0x12e9b15ad32faeb1a02f5ddd99254309faf5f2f8?tab=abi>`__
:return:
Dict for slippage analysis
"""
vault = cast(ERC4626Vault, vault)
# Anvil transactions should confirm relatively quickly,
# so do not wait for long time if it is going to crash
timeout = 10 if is_anvil(web3) else 60
# If we live in a forked universe, time can be whatever
block_number = web3.eth.block_number
now_ = get_block_timestamp(web3, block_number)
# Check for non-instant deposit/redemem cycle
try:
redemption_delay = vault.get_redemption_delay()
except NotImplementedError:
redemption_delay = "<unimplemented>"
logger.info("Vault %s (%s) redemption delay: %s", vault.name, vault.address, redemption_delay)
def _perform_tx(func: ContractFunction):
signed_tx = hot_wallet.sign_bound_call_with_new_nonce(
func,
web3=web3,
fill_gas_price=True,
)
logger.info(
"Broadcasting transaction %s(): %s",
func.fn_name,
signed_tx.hash.hex(),
)
raw_bytes = get_tx_broadcast_data(signed_tx)
web3.eth.send_raw_transaction(raw_bytes)
assert_transaction_success_with_explanation(web3, signed_tx.hash.hex(), timeout=timeout)
logger.info("Depositing...")
func_1, func_2 = approve_and_deposit_4626(
vault=vault,
from_=hot_wallet.address,
amount=deposit_value,
)
_perform_tx(func_1)
_perform_tx(func_2)
share_count = vault.share_token.fetch_balance_of(hot_wallet.address)
logger.info("We received %f %s", share_count, vault.share_token.symbol)
try:
redemption_over = vault.get_redemption_delay_over(hot_wallet.address)
redemption_delay = redemption_over - now_
redemption_delay_seconds = redemption_delay.total_seconds()
except NotImplementedError:
redemption_delay = "<unimplemented>"
redemption_delay_seconds = None
logger.info("After deposit, address has %s redemption over at: %s (%s seconds)", hot_wallet.address, redemption_delay, redemption_delay_seconds)
if redemption_delay_seconds:
logger.info("Simulating redeem delay. Using vault-given redemption_delay_seconds: %s", redemption_delay_seconds)
mine(web3, increase_timestamp=redemption_delay_seconds + 1)
elif redeem_wait_seconds:
logger.info("Simulating redeem delay. Using default redemption_delay_seconds: %s", redeem_wait_seconds)
mine(web3, increase_timestamp=redeem_wait_seconds)
func_3, func_4 = approve_and_redeem_4626(
vault=vault,
from_=hot_wallet.address,
amount=share_count,
)
before_redeemed_value = vault.denomination_token.fetch_balance_of(hot_wallet.address)
_perform_tx(func_3)
_perform_tx(func_4)
redeemed_value = vault.denomination_token.fetch_balance_of(hot_wallet.address) - before_redeemed_value
return {
"deposit_value": deposit_value,
"redeemed_value": redeemed_value,
"share_count": share_count,
}
def main():
"""Main entry point for the script."""
args = parse_args()
vault = args.vault
funding_wallet = args.usdc_forked_wallet
simulate = args.simulate
json_rpc_url = args.json_rpc_url
fork_block_number = args.fork_block_number
private_key = args.private_key
simplified_logging = args.simplified_logging
redeem_wait_seconds = args.redeem_wait_seconds
try:
deposit_value = Decimal(args.deposit_value)
except Exception as e:
raise ValueError(f"Invalid deposit value: {args.deposit_value}") from e
setup_console_logging(
default_log_level=os.environ.get("LOG_LEVEL", "info"),
simplified_logging=simplified_logging,
)
spec = VaultSpec.parse_string(vault)
chain_name = get_chain_name(spec.chain_id)
if not json_rpc_url:
json_rpc_url = read_json_rpc_url(spec.chain_id)
# Fish out provider namy without API KEY
web3_dummy = create_multi_provider_web3(json_rpc_url)
logger.info(
"Using JSON RPC provider %s for chain %s",
get_provider_name(web3_dummy.provider),
chain_name,
)
usdc_address = USDC_NATIVE_TOKEN.get(spec.chain_id)
assert usdc_address, f"USDC address not found for chain {spec.chain_id}"
if simulate:
# TODO: Currently only USDC autofunding supported
if funding_wallet is None:
funding_wallet = LARGE_USDC_HOLDERS[spec.chain_id]
assert funding_wallet, f"No large USDC folder for {chain_name} known. For simulation, please provide --usdc-forked-wallet address holding USDC balance"
# Addresses we need to take control to simulate GMX offchain Keeper fuctionality
unlocked_addresses = [
funding_wallet,
]
logger.info(f"Forking %s with Anvil", chain_name)
anvil = fork_network_anvil(
json_rpc_url,
unlocked_addresses=unlocked_addresses,
fork_block_number=fork_block_number, # Always simulate against a fixed statel
log_wait=False,
)
web3 = create_multi_provider_web3(
anvil.json_rpc_url,
default_http_timeout=(10.0, 60.0), # Increase default timeouts if your Anvil is slow
retries=0, # If Anvil RPC call fails, retries won't help
)
hot_wallet = create_fork_funded_wallet(
web3,
usdc_address=usdc_address,
large_usdc_holder=funding_wallet,
usdc_amount=deposit_value,
)
else:
logger.info("Base production deployment")
web3 = create_multi_provider_web3(json_rpc_url)
assert private_key, "Private key must be set in environment variable PRIVATE_KEY"
hot_wallet = None
raise NotImplementedError("TODO: Unfinished")
features = detect_vault_features(web3, spec.vault_address, verbose=False)
logger.info("Detected vault features: %s", features)
vault = create_vault_instance(
web3,
spec.vault_address,
features=features,
)
vault = cast(ERC4626Vault, vault)
assert vault.is_valid(), f"Vault contract does not look like ERC-4626: {vault.address}"
logger.info("Using vault %s (%s), our proxy class is %s", vault.name, vault.address, vault.__class__.__name__)
usdc = fetch_erc20_details(web3, usdc_address)
balance = usdc.fetch_balance_of(hot_wallet.address)
gas_balance = web3.eth.get_balance(hot_wallet.address)
assert vault.denomination_token == usdc, f"Vault denomination token {vault.denomination_token} does not match USDC {usdc.address}"
logger.info("Gas balance: %s ETH", gas_balance / 10**18)
logger.info("USDC balance: %s", balance)
assert balance >= deposit_value, f"Not enough USDC to deposit {deposit_value} (balance: {balance})"
logger.info("Depositing %s USDC to vault %s", deposit_value, spec.vault_address)
analysis = deposit_redeem(
web3=web3,
vault=vault,
hot_wallet=hot_wallet,
deposit_value=deposit_value,
redeem_wait_seconds=0 and redeem_wait_seconds,
)
slippage = (analysis["redeemed_value"] - analysis["deposit_value"]) / analysis["deposit_value"]
logger.info("Deposit value: %s %s", analysis["deposit_value"], usdc.symbol)
logger.info("Redeem value: %s %s", analysis["redeemed_value"], usdc.symbol)
logger.info("Share count: %s %s", analysis["share_count"], vault.share_token.symbol)
logger.info("Slippage: %.4f%%", slippage * 100)
logger.info("All done")
if __name__ == "__main__":
main()