diff --git a/MEV_Bot b/MEV_Bot index 65a10e8d..9c781977 160000 --- a/MEV_Bot +++ b/MEV_Bot @@ -1 +1 @@ -Subproject commit 65a10e8d433fd7388d0eab0e65c3ada8bffadeec +Subproject commit 9c7819777b32ee5eadc8529399e73c5202e82096 diff --git a/docs/04-configuration/MEV_EXECUTION_VALUE_SOURCES_AND_READINESS.md b/docs/04-configuration/MEV_EXECUTION_VALUE_SOURCES_AND_READINESS.md index 617dca44..764542ed 100644 --- a/docs/04-configuration/MEV_EXECUTION_VALUE_SOURCES_AND_READINESS.md +++ b/docs/04-configuration/MEV_EXECUTION_VALUE_SOURCES_AND_READINESS.md @@ -14,7 +14,8 @@ Use the verifier before promoting or committing execution-related config: ```bash bash scripts/verify/check-mev-execution-readiness.sh \ --config MEV_Bot/mev-platform/config.toml \ - --env-file config/mev-platform/mev-platform-backend-ct.env.example + --env-file config/mev-platform/mev-platform-backend-ct.env.example \ + --rpc-url https://eth.llamarpc.com ``` To compare local expectations with the live public admin API: @@ -50,6 +51,9 @@ bash scripts/verify/check-mev-execution-readiness.sh \ | `chains..execution.flash_loan_provider` | Real deployed venue/provider address | Arbitrage executor input | Still zero address in checked-in config | | `chains..execution.relay_url` | Config / operator choice | Relay submission target | Present in config | | `chains..factories[].router` for `uniswap_v2` / `sushiswap` | Authoritative DEX router addresses for the chain | Router-based swap-step encoding | Missing in checked-in config | +| `owner()` on executor contract | On-chain contract state | Must match signer EOA | Only checkable after real deploy | +| `pendingOwner()` on executor contract | On-chain contract state | Must be zero before live execution | Only checkable after real deploy | +| `paused()` on executor contract | On-chain contract state | Must be `false` before live execution | Only checkable after real deploy | ## What the current live API confirms @@ -112,6 +116,16 @@ bash scripts/deployment/deploy-mev-execution-contracts.sh \ This produces a JSON artifact under `reports/status/` and prints the exact non-secret config values to update next. +The helper also records the deployed executor's on-chain: + +- `owner()` +- `pendingOwner()` +- `paused()` +- `flashLoanProvider()` +- `treasury()` + +so you have an auditable deployment record instead of stdout-only notes. + ## Commit policy Safe to commit: diff --git a/scripts/deployment/deploy-mev-execution-contracts.sh b/scripts/deployment/deploy-mev-execution-contracts.sh index 66d41ae6..a0810a98 100755 --- a/scripts/deployment/deploy-mev-execution-contracts.sh +++ b/scripts/deployment/deploy-mev-execution-contracts.sh @@ -10,6 +10,8 @@ RPC_URL="${RPC_URL:-}" PRIVATE_KEY="${PRIVATE_KEY:-}" FLASH_LOAN_PROVIDER="${FLASH_LOAN_PROVIDER:-}" TREASURY="${TREASURY:-}" +EXECUTOR_OWNER="${EXECUTOR_OWNER:-}" +PAUSE_ON_DEPLOY="${PAUSE_ON_DEPLOY:-1}" CONFIG_PATH="${MEV_CONFIG_PATH:-$CONFIG_PATH_DEFAULT}" OUTPUT_PATH="${MEV_EXECUTION_DEPLOY_OUTPUT:-$OUTPUT_DEFAULT}" DRY_RUN=0 @@ -30,6 +32,8 @@ Required env or options: Optional env or options: TREASURY Treasury address; defaults to deployer in Foundry script + EXECUTOR_OWNER Optional final owner; deploy initiates 2-step ownership transfer + PAUSE_ON_DEPLOY 1/true to pause immediately after deploy (default: 1) MEV_CONFIG_PATH Config file to inspect for current values MEV_EXECUTION_DEPLOY_OUTPUT Output JSON path @@ -38,6 +42,8 @@ Options: --private-key KEY --flash-loan-provider ADDRESS --treasury ADDRESS + --executor-owner ADDRESS + --pause-on-deploy BOOL --config PATH --output PATH --dry-run @@ -63,6 +69,14 @@ while [[ $# -gt 0 ]]; do TREASURY="$2" shift 2 ;; + --executor-owner) + EXECUTOR_OWNER="$2" + shift 2 + ;; + --pause-on-deploy) + PAUSE_ON_DEPLOY="$2" + shift 2 + ;; --config) CONFIG_PATH="$2" shift 2 @@ -97,6 +111,7 @@ require_cmd() { require_cmd forge require_cmd jq require_cmd python3 +require_cmd cast if [[ -z "$RPC_URL" ]]; then echo "RPC_URL is required" >&2 @@ -122,6 +137,10 @@ mkdir -p "$(dirname "$OUTPUT_PATH")" CHAIN_ID="$(cast chain-id --rpc-url "$RPC_URL")" BROADCAST_PATH="$CONTRACTS_DIR/broadcast/Deploy.s.sol/$CHAIN_ID/run-latest.json" +DEPLOYER_ADDRESS="" +if [[ -n "$PRIVATE_KEY" ]]; then + DEPLOYER_ADDRESS="$(cast wallet address --private-key "$PRIVATE_KEY")" +fi cat <&2 + exit 1 +fi + +ONCHAIN_OWNER="$(cast call "$EXECUTOR_CONTRACT" "owner()(address)" --rpc-url "$RPC_URL" 2>/dev/null || true)" +ONCHAIN_PENDING_OWNER="$(cast call "$EXECUTOR_CONTRACT" "pendingOwner()(address)" --rpc-url "$RPC_URL" 2>/dev/null || true)" +ONCHAIN_PAUSED="$(cast call "$EXECUTOR_CONTRACT" "paused()(bool)" --rpc-url "$RPC_URL" 2>/dev/null || true)" +ONCHAIN_PROVIDER="$(cast call "$EXECUTOR_CONTRACT" "flashLoanProvider()(address)" --rpc-url "$RPC_URL" 2>/dev/null || true)" +ONCHAIN_TREASURY="$(cast call "$EXECUTOR_CONTRACT" "treasury()(address)" --rpc-url "$RPC_URL" 2>/dev/null || true)" + +python3 - "$OUTPUT_PATH" "$CONFIG_PATH" "$BROADCAST_PATH" "$CHAIN_ID" "$EXECUTOR_CONTRACT" "$UNISWAP_V2_ADAPTER" "$FLASH_LOAN_PROVIDER" "$TREASURY" "$DEPLOYER_ADDRESS" "$EXECUTOR_OWNER" "$PAUSE_ON_DEPLOY" "$ONCHAIN_OWNER" "$ONCHAIN_PENDING_OWNER" "$ONCHAIN_PAUSED" "$ONCHAIN_PROVIDER" "$ONCHAIN_TREASURY" <<'PY' import json -import re import sys from pathlib import Path -broadcast_path = Path(sys.argv[1]) -output_path = Path(sys.argv[2]) -config_path = Path(sys.argv[3]) -flash_loan_provider = sys.argv[4] -treasury = sys.argv[5] or None -chain_id = int(sys.argv[6]) - -data = json.loads(broadcast_path.read_text()) -transactions = data.get("transactions", []) - -executor = None -adapter = None -for tx in transactions: - contract_name = tx.get("contractName") - address = tx.get("contractAddress") - if contract_name == "ArbitrageExecutor" and address: - executor = address - elif contract_name == "UniswapV2Adapter" and address: - adapter = address - -if not executor or not adapter: - raise SystemExit("Could not extract deployed contract addresses from broadcast artifact") +output_path = Path(sys.argv[1]) +config_path = Path(sys.argv[2]) +broadcast_path = Path(sys.argv[3]) +chain_id = int(sys.argv[4]) +executor_contract = sys.argv[5] +uniswap_v2_adapter = sys.argv[6] +flash_loan_provider = sys.argv[7] +treasury = sys.argv[8] or None +deployer_address = sys.argv[9] or None +executor_owner = sys.argv[10] or None +pause_on_deploy = sys.argv[11] +onchain_owner = sys.argv[12] or None +onchain_pending_owner = sys.argv[13] or None +onchain_paused = sys.argv[14] or None +onchain_provider = sys.argv[15] or None +onchain_treasury = sys.argv[16] or None artifact = { "chain_id": chain_id, "broadcast_artifact": str(broadcast_path), "config_path": str(config_path), - "executor_contract": executor, - "uniswap_v2_adapter": adapter, + "executor_contract": executor_contract, + "uniswap_v2_adapter": uniswap_v2_adapter, "flash_loan_provider": flash_loan_provider, "treasury": treasury, + "deployer_address": deployer_address, + "requested_executor_owner": executor_owner, + "pause_on_deploy": pause_on_deploy, + "onchain": { + "owner": onchain_owner, + "pending_owner": onchain_pending_owner, + "paused": onchain_paused, + "flash_loan_provider": onchain_provider, + "treasury": onchain_treasury, + }, } output_path.write_text(json.dumps(artifact, indent=2) + "\n") @@ -206,12 +253,15 @@ print("") print(json.dumps(artifact, indent=2)) print("") print("next config values:") -print(f'chains.{chain_id}.execution.executor_contract = "{executor}"') +print(f'chains.{chain_id}.execution.executor_contract = "{executor_contract}"') print(f'chains.{chain_id}.execution.flash_loan_provider = "{flash_loan_provider}"') print("") print("next operator checks:") -print(f"- verify code exists at {executor}") -print(f"- confirm the signer EOA is the executor owner") +print(f"- verify code exists at {executor_contract}") +print(f"- verify owner(): {onchain_owner}") +if onchain_pending_owner and onchain_pending_owner.lower() != "0x0000000000000000000000000000000000000000": + print(f"- pendingOwner() is set to {onchain_pending_owner}; that address must call acceptOwnership()") +print(f"- verify paused(): {onchain_paused}") print("- set router addresses for every V2-style dex used by execution") print("- keep MEV_SUBMIT_DISABLED=1 until signer/config readiness is green") PY diff --git a/scripts/verify/check-mev-execution-readiness.sh b/scripts/verify/check-mev-execution-readiness.sh index 19f67ac3..5c61555e 100755 --- a/scripts/verify/check-mev-execution-readiness.sh +++ b/scripts/verify/check-mev-execution-readiness.sh @@ -10,6 +10,7 @@ ENV_PATH="${MEV_ENV_FILE:-$ENV_DEFAULT}" BASE_URL="${MEV_BASE_URL:-}" API_KEY="${MEV_API_KEY:-}" CHAIN_ID="${MEV_CHAIN_ID:-1}" +RPC_URL_OVERRIDE="${MEV_RPC_URL:-}" usage() { cat <<'EOF' @@ -26,6 +27,7 @@ Options: --base URL Optional admin API base URL, e.g. https://mev.defi-oracle.io --api-key KEY Optional API key for protected routes --chain ID Chain ID to inspect (default: 1) + --rpc-url URL Optional RPC URL for on-chain executor checks -h, --help Show this help Exit codes: @@ -57,6 +59,10 @@ while [[ $# -gt 0 ]]; do CHAIN_ID="$2" shift 2 ;; + --rpc-url) + RPC_URL_OVERRIDE="$2" + shift 2 + ;; -h|--help) usage exit 0 @@ -79,7 +85,7 @@ if [[ ! -f "$ENV_PATH" ]]; then exit 2 fi -python3 - "$CONFIG_PATH" "$ENV_PATH" "$BASE_URL" "$API_KEY" "$CHAIN_ID" <<'PY' +python3 - "$CONFIG_PATH" "$ENV_PATH" "$BASE_URL" "$API_KEY" "$CHAIN_ID" "$RPC_URL_OVERRIDE" <<'PY' import json import os import subprocess @@ -93,6 +99,7 @@ env_path = Path(sys.argv[2]) base_url = sys.argv[3].rstrip("/") api_key = sys.argv[4] chain_id = sys.argv[5] +rpc_url_override = sys.argv[6] def parse_env_file(path: Path) -> dict[str, str]: @@ -117,6 +124,19 @@ def is_zero_address(value: str | None) -> bool: } +def run_cast_call(address: str, signature: str, rpc_url: str) -> str | None: + try: + result = subprocess.run( + ["cast", "call", address, signature, "--rpc-url", rpc_url], + check=True, + capture_output=True, + text=True, + ) + except Exception: # noqa: BLE001 + return None + return result.stdout.strip() or None + + config = tomllib.loads(config_path.read_text()) env_values = parse_env_file(env_path) chain_key = str(chain_id) @@ -154,6 +174,7 @@ else: executor_contract = execution.get("executor_contract", "") flash_loan_provider = execution.get("flash_loan_provider", "") relay_url = execution.get("relay_url", "") + rpc_url = rpc_url_override or chain.get("rpc_url", "") add_row( f"chains.{chain_key}.execution.executor_contract", str(config_path), @@ -198,6 +219,54 @@ else: if is_zero_address(router): issues.append(f"chain {chain_key}: router missing for dex {dex}") + if rpc_url and not is_zero_address(executor_contract): + owner = run_cast_call(executor_contract, "owner()(address)", rpc_url) + pending_owner = run_cast_call(executor_contract, "pendingOwner()(address)", rpc_url) + paused = run_cast_call(executor_contract, "paused()(bool)", rpc_url) + onchain_provider = run_cast_call(executor_contract, "flashLoanProvider()(address)", rpc_url) + onchain_treasury = run_cast_call(executor_contract, "treasury()(address)", rpc_url) + + if owner is None: + add_row(f"chains.{chain_key}.onchain.owner", rpc_url, "(unavailable)", "missing") + issues.append(f"chain {chain_key}: failed to read owner() from executor") + else: + add_row(f"chains.{chain_key}.onchain.owner", rpc_url, owner, "ok") + if signer_address and owner.lower() != signer_address.lower(): + issues.append(f"chain {chain_key}: executor owner does not match signer address") + + if pending_owner is None: + add_row(f"chains.{chain_key}.onchain.pendingOwner", rpc_url, "(unavailable)", "missing") + issues.append(f"chain {chain_key}: failed to read pendingOwner() from executor") + else: + pending_status = "ok" if is_zero_address(pending_owner) else "blocking" + add_row(f"chains.{chain_key}.onchain.pendingOwner", rpc_url, pending_owner, pending_status) + if not is_zero_address(pending_owner): + issues.append(f"chain {chain_key}: pendingOwner must accept ownership") + + if paused is None: + add_row(f"chains.{chain_key}.onchain.paused", rpc_url, "(unavailable)", "missing") + issues.append(f"chain {chain_key}: failed to read paused() from executor") + else: + paused_normalized = paused.lower() + paused_status = "ok" if paused_normalized == "false" else "blocking" + add_row(f"chains.{chain_key}.onchain.paused", rpc_url, paused, paused_status) + if paused_normalized != "false": + issues.append(f"chain {chain_key}: executor is paused") + + if onchain_provider is None: + add_row(f"chains.{chain_key}.onchain.flashLoanProvider", rpc_url, "(unavailable)", "missing") + issues.append(f"chain {chain_key}: failed to read flashLoanProvider() from executor") + else: + provider_status = "ok" if onchain_provider.lower() == flash_loan_provider.lower() else "blocking" + add_row(f"chains.{chain_key}.onchain.flashLoanProvider", rpc_url, onchain_provider, provider_status) + if onchain_provider.lower() != flash_loan_provider.lower(): + issues.append(f"chain {chain_key}: on-chain flashLoanProvider does not match config") + + if onchain_treasury is None: + add_row(f"chains.{chain_key}.onchain.treasury", rpc_url, "(unavailable)", "missing") + else: + add_row(f"chains.{chain_key}.onchain.treasury", rpc_url, onchain_treasury, "ok") + print("MEV execution readiness") print(f"config: {config_path}") print(f"env: {env_path}")