diff --git a/.env.example b/.env.example index e5e7d110..063dcde7 100644 --- a/.env.example +++ b/.env.example @@ -44,6 +44,22 @@ export BSC_TESTNET_VERIFIER_URL="https://api.etherscan.io/v2/api?chainid=97" export OPTIMISM_SEPOLIA_VERIFIER_URL="https://api.etherscan.io/v2/api?chainid=11155420" export SONEIUM_TESTNET_VERIFIER_URL="https://soneium-minato.blockscout.com/api" +# Seismic +# Verifier URL must be the FULL socialscan command_api path, not the bare host. +export SEISMIC_TESTNET_RPC_URL=https://testnet-1.seismictest.net/rpc +export SEISMIC_TESTNET_VERIFIER_URL=https://api.socialscan.io/seismic-testnet/v1/explorer/command_api/contract +# Set when Seismic mainnet is live (deploy targets fail closed until then). +# export SEISMIC_MAINNET_RPC_URL= +# export SEISMIC_MAINNET_CHAIN_ID= +# export SEISMIC_MAINNET_VERIFIER_URL= + +# Seismic post-deploy ops (configure-extension-*, set-contract-key-*) +# Contract encryption keys live in 1Password, never here. +export SWAP_FACILITY= +export EXTENSION_PROXY= +export PORTAL= +export LIMIT_ORDER_PROTOCOL= + # Extension-specific deployment configuration # Uncomment and set the variables for the extension you want to deploy diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3d63fced..0c7c04fd 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,10 +1,10 @@ +# Disabled on the Seismic branch: stock forge cannot compile shielded types +# (suint256) and this branch never merges to main. CI runs test-seismic.yml +# instead; a manual dispatch of this workflow will fail at compile. name: Forge Coverage on: - push: - branches: - - main - pull_request: + workflow_dispatch: permissions: write-all diff --git a/.github/workflows/test-gas.yml b/.github/workflows/test-gas.yml index 0be21a2e..be378347 100644 --- a/.github/workflows/test-gas.yml +++ b/.github/workflows/test-gas.yml @@ -1,10 +1,10 @@ +# Disabled on the Seismic branch: stock forge cannot compile shielded types +# (suint256) and this branch never merges to main. CI runs test-seismic.yml +# instead; a manual dispatch of this workflow will fail at compile. name: Forge Tests Gas Report on: - push: - branches: - - main - pull_request: + workflow_dispatch: permissions: write-all diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 9a8163c8..7243c002 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -1,10 +1,11 @@ +# Disabled on the Seismic branch: stock forge cannot compile shielded types +# (suint256), mainnet-fork RPCs lack eth_getFlaggedStorageAt, and this branch +# never merges to main. CI runs test-seismic.yml instead; a manual dispatch of +# this workflow will fail at compile. name: Forge Integration Tests on: - push: - branches: - - main - pull_request: + workflow_dispatch: permissions: write-all diff --git a/.github/workflows/test-seismic.yml b/.github/workflows/test-seismic.yml new file mode 100644 index 00000000..875e37f8 --- /dev/null +++ b/.github/workflows/test-seismic.yml @@ -0,0 +1,80 @@ +# Seismic CI — the only workflow that can compile this branch (shielded types +# need the sforge/ssolc toolchain; see scripts/seismic-env.sh). Installs the +# toolchain repo-locally into .seismic-toolchain (the path the Makefile and +# .husky/pre-commit auto-detect), cached on the pinned sforge version. +name: Seismic Tests + +on: + push: + branches: + - feat/seismic + - seismic-audit-readiness + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + # Pinned toolchain version: bump deliberately (also rolls the cache key). + SFORGE_VERSION: 1.3.5-v0.2.0 + # Toolchain-cache layout version: bump to invalidate caches when the contents of + # .seismic-toolchain change shape (independent of SFORGE_VERSION). v2 added the + # co-located `ssolc` binary; v1 caches only held sforge/sanvil/scast, so cache-hit + # runs failed at compile time with "`ssolc` not found in PATH". + TOOLCHAIN_CACHE_VERSION: v2 + +jobs: + test: + name: Unit tests + sizes (sforge) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache Seismic toolchain + id: toolchain-cache + uses: actions/cache@v4 + with: + path: .seismic-toolchain + key: seismic-toolchain-${{ runner.os }}-sforge-${{ env.SFORGE_VERSION }}-${{ env.TOOLCHAIN_CACHE_VERSION }} + + - name: Install Seismic toolchain + if: steps.toolchain-cache.outputs.cache-hit != 'true' + run: | + set -euo pipefail + export FOUNDRY_DIR="$PWD/.seismic-toolchain" + export FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" + export PATH="$FOUNDRY_BIN_DIR:$PATH" + curl -L -H "Accept: application/vnd.github.v3.raw" \ + "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash + sfoundryup + # sfoundryup installs `ssolc` to /usr/local/bin (its install_ssolc ignores + # FOUNDRY_BIN_DIR), which is NOT under the cached .seismic-toolchain dir — so + # the compiler is lost on cache-hit runs. Co-locate it in the toolchain bin + # (the only dir the Makefile adds to PATH) so it's cached and restored too. + if [ ! -x "$FOUNDRY_BIN_DIR/ssolc" ]; then + cp "$(command -v ssolc)" "$FOUNDRY_BIN_DIR/ssolc" + fi + + - name: Assert pinned toolchain (sforge + ssolc) + run: | + ./.seismic-toolchain/bin/sforge --version + ./.seismic-toolchain/bin/sforge --version | grep -F "$SFORGE_VERSION" \ + || { echo "sfoundryup installed sforge != $SFORGE_VERSION (installer tracks upstream latest); bump SFORGE_VERSION deliberately"; exit 1; } + # ssolc must live in the cached toolchain dir, else cache-hit runs fail at + # compile time with "`ssolc` not found in PATH" (the Makefile only prepends + # .seismic-toolchain/bin to PATH). Fail fast here instead. + test -x ./.seismic-toolchain/bin/ssolc \ + || { echo "ssolc missing from .seismic-toolchain/bin (stale cache predating ssolc co-location); bump TOOLCHAIN_CACHE_VERSION to invalidate"; exit 1; } + ./.seismic-toolchain/bin/ssolc --version + + - name: Run unit tests (seismic profile) + run: make tests profile=seismic + + - name: Run in-process Seismic integration suite (real precompiles) + run: make integration-seismic profile=seismic + + - name: Check contract sizes (EIP-170) + run: make sizes profile=seismic diff --git a/.gitignore b/.gitignore index 838c5a9a..d8e7843e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ out/ coverage/ lcov.info -# Docs +# Docs (generated `forge doc` output, design notes, and drafts — all internal, not tracked) docs/ # Dotenv file diff --git a/.husky/pre-commit b/.husky/pre-commit index e9e22bcb..29cda075 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,12 +1,12 @@ -# If the repo-local Seismic toolchain is installed (see scripts/seismic-env.sh), -# put sforge/ssolc on PATH and pin FOUNDRY_PROFILE=seismic so the test step -# below compiles shielded-type code (suint256, etc.) that stock solc rejects. -# When the toolchain isn't present, the hook falls back to stock forge. -if [ -x ".seismic-toolchain/bin/sforge" ]; then - PATH="$PWD/.seismic-toolchain/bin:$PATH" - export PATH - FOUNDRY_PROFILE=seismic - export FOUNDRY_PROFILE +# Shielded-type sources need the repo-local Seismic toolchain — fail fast with the install hint instead of a stock-forge parse error. +if [ ! -x ".seismic-toolchain/bin/sforge" ]; then + echo "Seismic toolchain missing — run: source scripts/seismic-env.sh && sfoundryup" >&2 + exit 1 fi +PATH="$PWD/.seismic-toolchain/bin:$PATH" +export PATH +FOUNDRY_PROFILE=seismic +export FOUNDRY_PROFILE + npm run lint-staged && npm test diff --git a/Makefile b/Makefile index 06aeb5e0..871c45bf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,17 @@ # (-include to ignore error if it does not exist) -include .env -# dapp deps +# --- Seismic toolchain (sforge/ssolc) ----------------------------------------- +# Shielded types need the Seismic fork: prepend the repo-local install if present, else fail fast via $(require-toolchain); recipes can't source seismic-env.sh under make's sh. +ifneq ($(wildcard .seismic-toolchain/bin/sforge),) +export PATH := $(CURDIR)/.seismic-toolchain/bin:$(PATH) +endif + +define require-toolchain +@command -v sforge >/dev/null 2>&1 || { echo "Seismic toolchain missing — run: source scripts/seismic-env.sh && sfoundryup"; exit 1; } +endef + +# dapp deps (stock forge; does not compile sources) update:; forge update # Default to actual deployment (not simulation) @@ -17,86 +27,89 @@ else BROADCAST_ONLY_FLAGS = --broadcast endif -# Deployment helpers -deploy-local :; FOUNDRY_PROFILE=production forge script script/Deploy.s.sol --rpc-url localhost $(BROADCAST_ONLY_FLAGS) -v -deploy-sepolia :; FOUNDRY_PROFILE=production forge script script/Deploy.s.sol --rpc-url sepolia $(BROADCAST_ONLY_FLAGS) -vvv +# Not runnable on this branch: crytic-compile cannot ingest mercury/ssolc builds (shielded types). +slither: + @echo "slither cannot ingest mercury/ssolc builds; last clean static-analysis baseline is merge-base 87a2f42 on main" + @exit 1 -# Run slither -slither :; FOUNDRY_PROFILE=production forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --compile-force-framework foundry --ignore-compile --sarif results.sarif --config-file slither.config.json . +# Common tasks — honor an inherited FOUNDRY_PROFILE (e.g. from .husky/pre-commit); default "seismic". +profile ?= $(if $(FOUNDRY_PROFILE),$(FOUNDRY_PROFILE),seismic) -# Common tasks -# Honor an inherited FOUNDRY_PROFILE (e.g. exported by .husky/pre-commit when the -# Seismic toolchain is detected). Falls back to "default" when neither is set. -profile ?= $(if $(FOUNDRY_PROFILE),$(FOUNDRY_PROFILE),default) +# sforge for the seismic profile (mercury EVM, ssolc); stock forge otherwise. +FORGE_BIN = $(if $(filter seismic,$(profile)),sforge,forge) build: - @./build.sh -p production + $(require-toolchain) + @./build.sh -p $(profile) tests: + $(require-toolchain) @./test.sh -p $(profile) fuzz: + $(require-toolchain) @./test.sh -t testFuzz -p $(profile) +# Fork RPCs lack eth_getFlaggedStorageAt, so integration tests only run against a Seismic +# devnet; FOUNDRY_NO_MATCH_PATH re-includes test/integration/** (excluded by the profile). integration: - @./test.sh -d test/integration -p $(profile) - -invariant: - @./test.sh -d test/invariant -p $(profile) + $(require-toolchain) + @if [ -z "$(SEISMIC_DEVNET_RPC_URL)" ]; then \ + echo "integration tests cannot run locally (fork RPC lacks eth_getFlaggedStorageAt); run against Seismic devnet"; \ + exit 1; \ + fi + SEISMIC_DEVNET_RPC_URL=$(SEISMIC_DEVNET_RPC_URL) FOUNDRY_NO_MATCH_PATH='no_match_nothing/**' ./test.sh -d test/integration -p $(profile) + +# In-process Seismic integration suite (real precompiles, no node needed). +integration-seismic: + $(require-toolchain) + FOUNDRY_NO_MATCH_PATH='no_match_nothing/**' FOUNDRY_PROFILE=$(profile) $(FORGE_BIN) test --match-path 'test/integration/seismic/*' -vv + +# Full sanvil E2E: TxSeismic key install, signed-read gating, off-chain decryption. +e2e-sanvil: + $(require-toolchain) + bash test/integration/seismic/run-sanvil-e2e.sh + +# Read-only status checklist against the live chain-5124 deployment. +check-live-seismic-testnet: + bash test/integration/seismic/check-live-testnet.sh coverage: - FOUNDRY_PROFILE=$(profile) forge coverage --report lcov && lcov --extract lcov.info -o lcov.info 'src/*' --ignore-errors inconsistent && genhtml lcov.info -o coverage + $(require-toolchain) + FOUNDRY_PROFILE=$(profile) $(FORGE_BIN) coverage --report lcov && lcov --extract lcov.info -o lcov.info 'src/*' --ignore-errors inconsistent && genhtml lcov.info -o coverage gas-report: - FOUNDRY_PROFILE=$(profile) forge test --force --gas-report > gasreport.ansi + $(require-toolchain) + FOUNDRY_PROFILE=$(profile) $(FORGE_BIN) test --force --gas-report > gasreport.ansi sizes: - @./build.sh -p production -s + $(require-toolchain) + @./build.sh -p $(profile) -s clean: forge clean && rm -rf ./abi && rm -rf ./bytecode && rm -rf ./types -# -# -# DEPLOY -# -# +# --- DEPLOY (Seismic only; this branch never merges back — other-chain targets live on main) --- # --- Seismic (mercury/ssolc) verification -------------------------------------- -# Inline `sforge script --verify` can't verify mercury builds as of the moment: the socialscan -# explorer rejects `evmVersion: mercury` in the standard-json and there is no -# forge flag to strip it. So seismic targets broadcast only, then auto-verify -# every contract in the broadcast via script/verify-seismic.py (needs python3 + -# jq; no ssolc/sforge needed — it reads out/build-info). +# socialscan rejects `evmVersion: mercury`, so inline `--verify` can't work; targets broadcast only, then script/verify-seismic.py auto-verifies the whole broadcast (python3 + jq). # Testnet (chain 5124) — full socialscan verify endpoint. SEISMIC_TESTNET_CHAIN_ID ?= 5124 SEISMIC_TESTNET_VERIFIER_URL ?= https://api.socialscan.io/seismic-testnet/v1/explorer/command_api/contract SEISMIC_VERIFY_TESTNET = VERIFIER_URL=$(SEISMIC_TESTNET_VERIFIER_URL) python3 script/verify-seismic.py $(1) $(2) -# Mainnet — set SEISMIC_MAINNET_{RPC_URL,CHAIN_ID,VERIFIER_URL} in .env before use. -# The URL below is the expected socialscan mainnet path; confirm it when Seismic -# mainnet is live. CHAIN_ID defaults empty so the target fails closed until set. +# Mainnet — expected socialscan path (confirm at launch); CHAIN_ID defaults empty so targets fail closed until .env sets SEISMIC_MAINNET_{RPC_URL,CHAIN_ID,VERIFIER_URL}. SEISMIC_MAINNET_CHAIN_ID ?= SEISMIC_MAINNET_VERIFIER_URL ?= https://api.socialscan.io/seismic/v1/explorer/command_api/contract SEISMIC_VERIFY_MAINNET = VERIFIER_URL=$(SEISMIC_MAINNET_VERIFIER_URL) python3 script/verify-seismic.py $(1) $(2) # $(call SEISMIC_VERIFY_{TESTNET,MAINNET},,) -# Default deploy flags = broadcast + inline etherscan verify (works on normal -# chains). Seismic targets override DEPLOY_FLAGS to broadcast-only + POST_DEPLOY. +# Seismic targets override DEPLOY_FLAGS to broadcast-only + POST_DEPLOY (see above). DEPLOY_FLAGS = $(BROADCAST_FLAGS) --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} POST_DEPLOY ?= -deploy-yield-to-one: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ - forge script script/deploy/DeployYieldToOne.s.sol:DeployYieldToOne \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) - -deploy-yield-to-one-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-yield-to-one-sepolia: deploy-yield-to-one - deploy-yield-to-one-forced-transfer: + $(require-toolchain) FOUNDRY_PROFILE=seismic PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ sforge script script/deploy/DeployYieldToOneForcedTransfer.s.sol:DeployYieldToOneForcedTransfer \ --rpc-url $(RPC_URL) \ @@ -104,7 +117,9 @@ deploy-yield-to-one-forced-transfer: --skip test --slow --non-interactive $(DEPLOY_FLAGS) $(POST_DEPLOY) +# local node must be sanvil (stock anvil lacks the mercury EVM / shielded types) deploy-yield-to-one-forced-transfer-local: RPC_URL=$(LOCALHOST_RPC_URL) +deploy-yield-to-one-forced-transfer-local: DEPLOY_FLAGS=$(BROADCAST_ONLY_FLAGS) deploy-yield-to-one-forced-transfer-local: deploy-yield-to-one-forced-transfer # Re-verify the latest seismic-testnet broadcast without redeploying. @@ -115,371 +130,48 @@ verify-yield-to-one-forced-transfer-seismic-testnet: verify-yield-to-one-forced-transfer-seismic-mainnet: $(call SEISMIC_VERIFY_MAINNET,DeployYieldToOneForcedTransfer.s.sol,$(SEISMIC_MAINNET_CHAIN_ID)) -# Seismic mainnet: broadcast only (inline --verify can't reach the explorer), -# then auto-verify every deployed contract from the broadcast. Requires -# SEISMIC_MAINNET_{RPC_URL,CHAIN_ID,VERIFIER_URL} in .env; fails closed if unset. +# Seismic mainnet: broadcast only, then auto-verify from the broadcast (fails closed until .env is set). deploy-yield-to-one-forced-transfer-seismic-mainnet: RPC_URL=$(SEISMIC_MAINNET_RPC_URL) deploy-yield-to-one-forced-transfer-seismic-mainnet: DEPLOY_FLAGS=$(BROADCAST_ONLY_FLAGS) deploy-yield-to-one-forced-transfer-seismic-mainnet: POST_DEPLOY=$(call SEISMIC_VERIFY_MAINNET,DeployYieldToOneForcedTransfer.s.sol,$(SEISMIC_MAINNET_CHAIN_ID)) deploy-yield-to-one-forced-transfer-seismic-mainnet: deploy-yield-to-one-forced-transfer -deploy-yield-to-one-forced-transfer-citrea: RPC_URL=$(CITREA_RPC_URL) -deploy-yield-to-one-forced-transfer-citrea: VERIFIER="custom" -deploy-yield-to-one-forced-transfer-citrea: VERIFIER_URL=${CITREA_VERIFIER_URL} -deploy-yield-to-one-forced-transfer-citrea: deploy-yield-to-one-forced-transfer - -# Seismic testnet: broadcast only (inline --verify can't reach the explorer), -# then auto-verify every deployed contract from the broadcast. +# Seismic testnet: broadcast only, then auto-verify from the broadcast. deploy-yield-to-one-forced-transfer-seismic-testnet: RPC_URL=$(SEISMIC_TESTNET_RPC_URL) deploy-yield-to-one-forced-transfer-seismic-testnet: DEPLOY_FLAGS=$(BROADCAST_ONLY_FLAGS) deploy-yield-to-one-forced-transfer-seismic-testnet: POST_DEPLOY=$(call SEISMIC_VERIFY_TESTNET,DeployYieldToOneForcedTransfer.s.sol,$(SEISMIC_TESTNET_CHAIN_ID)) deploy-yield-to-one-forced-transfer-seismic-testnet: deploy-yield-to-one-forced-transfer -deploy-yield-to-one-forced-transfer-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-yield-to-one-forced-transfer-sepolia: VERIFIER="etherscan" -deploy-yield-to-one-forced-transfer-sepolia: VERIFIER_URL=${SEPOLIA_VERIFIER_URL} -deploy-yield-to-one-forced-transfer-sepolia: deploy-yield-to-one-forced-transfer - -deploy-yield-to-all: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ - forge script script/deploy/DeployYieldToAllWithFee.s.sol:DeployYieldToAllWithFee \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) - -deploy-yield-to-all-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-yield-to-all-sepolia: deploy-yield-to-all - -deploy-m-earner-manager: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ - forge script script/deploy/DeployMEarnerManager.s.sol:DeployMEarnerManager \ - --private-key $(PRIVATE_KEY) \ - --rpc-url $(RPC_URL) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) - -deploy-m-earner-manager-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-m-earner-manager-sepolia: deploy-m-earner-manager - deploy-jmi-extension: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ - forge script script/deploy/DeployJMIExtension.s.sol:DeployJMIExtension \ + $(require-toolchain) + FOUNDRY_PROFILE=seismic PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_NAME=$(EXTENSION_NAME) \ + sforge script script/deploy/DeployJMIExtension.s.sol:DeployJMIExtension \ --rpc-url $(RPC_URL) \ --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) - -deploy-jmi-extension-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-jmi-extension-sepolia: deploy-jmi-extension + --skip test --slow --non-interactive $(DEPLOY_FLAGS) + $(POST_DEPLOY) -deploy-swap-adapter: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) \ - forge script script/deploy/DeploySwapAdapter.s.sol:DeploySwapAdapter \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive \ - $(BROADCAST_FLAGS) --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} - -deploy-swap-adapter-local: RPC_URL=$(LOCALHOST_RPC_URL) -deploy-swap-adapter-local: deploy-swap-adapter - -deploy-swap-adapter-mainnet: RPC_URL=$(MAINNET_RPC_URL) -deploy-swap-adapter-mainnet: VERIFIER="etherscan" -deploy-swap-adapter-mainnet: VERIFIER_URL=${MAINNET_VERIFIER_URL} -deploy-swap-adapter-mainnet: deploy-swap-adapter - -deploy-swap-adapter-arbitrum: RPC_URL=$(ARBITRUM_RPC_URL) -deploy-swap-adapter-arbitrum: VERIFIER="etherscan" -deploy-swap-adapter-arbitrum: VERIFIER_URL=${ARBITRUM_VERIFIER_URL} -deploy-swap-adapter-arbitrum: deploy-swap-adapter - -deploy-swap-adapter-soneium: RPC_URL=$(SONEIUM_RPC_URL) -deploy-swap-adapter-soneium: VERIFIER="blockscout" -deploy-swap-adapter-soneium: VERIFIER_URL=${SONEIUM_VERIFIER_URL} -deploy-swap-adapter-soneium: deploy-swap-adapter - -deploy-swap-adapter-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-swap-adapter-sepolia: VERIFIER="etherscan" -deploy-swap-adapter-sepolia: VERIFIER_URL=${SEPOLIA_VERIFIER_URL} -deploy-swap-adapter-sepolia: deploy-swap-adapter - -deploy-swap-adapter-arbitrum-sepolia: RPC_URL=$(ARBITRUM_SEPOLIA_RPC_URL) -deploy-swap-adapter-arbitrum-sepolia: VERIFIER="etherscan" -deploy-swap-adapter-arbitrum-sepolia: VERIFIER_URL=${ARBITRUM_SEPOLIA_VERIFIER_URL} -deploy-swap-adapter-arbitrum-sepolia: deploy-swap-adapter - -deploy-swap-facility: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) PAUSER=$(PAUSER) \ - forge script script/deploy/DeploySwapFacility.s.sol:DeploySwapFacility \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive -v \ - $(BROADCAST_FLAGS) --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} - -deploy-swap-facility-local: RPC_URL=$(LOCALHOST_RPC_URL) -deploy-swap-facility-local: deploy-swap-facility - -deploy-swap-facility-mainnet: RPC_URL=$(MAINNET_RPC_URL) -deploy-swap-facility-mainnet: VERIFIER="etherscan" -deploy-swap-facility-mainnet: VERIFIER_URL=${MAINNET_VERIFIER_URL} -deploy-swap-facility-mainnet: deploy-swap-facility - -deploy-swap-facility-arbitrum: RPC_URL=$(ARBITRUM_RPC_URL) -deploy-swap-facility-arbitrum: VERIFIER="etherscan" -deploy-swap-facility-arbitrum: VERIFIER_URL=${ARBITRUM_VERIFIER_URL} -deploy-swap-facility-arbitrum: deploy-swap-facility - -deploy-swap-facility-optimism: RPC_URL=$(OPTIMISM_RPC_URL) -deploy-swap-facility-optimism: VERIFIER="etherscan" -deploy-swap-facility-optimism: VERIFIER_URL=${OPTIMISM_VERIFIER_URL} -deploy-swap-facility-optimism: deploy-swap-facility - -deploy-swap-facility-hyperliquid: RPC_URL=$(HYPERLIQUID_RPC_URL) -deploy-swap-facility-hyperliquid: VERIFIER="etherscan" -deploy-swap-facility-hyperliquid: VERIFIER_URL=${HYPERLIQUID_VERIFIER_URL} -deploy-swap-facility-hyperliquid: deploy-swap-facility - -deploy-swap-facility-plume: RPC_URL=$(PLUME_RPC_URL) -deploy-swap-facility-plume: VERIFIER="blockscout" -deploy-swap-facility-plume: VERIFIER_URL=${PLUME_VERIFIER_URL} -deploy-swap-facility-plume: deploy-swap-facility - -deploy-swap-facility-bsc: RPC_URL=$(BSC_RPC_URL) -deploy-swap-facility-bsc: VERIFIER="etherscan" -deploy-swap-facility-bsc: VERIFIER_URL=${BSC_VERIFIER_URL} -deploy-swap-facility-bsc: deploy-swap-facility - -deploy-swap-facility-mantra: RPC_URL=$(MANTRA_RPC_URL) -deploy-swap-facility-mantra: VERIFIER="blockscout" -deploy-swap-facility-mantra: VERIFIER_URL=${MANTRA_VERIFIER_URL} -deploy-swap-facility-mantra: deploy-swap-facility - -deploy-swap-facility-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -deploy-swap-facility-sepolia: VERIFIER="etherscan" -deploy-swap-facility-sepolia: VERIFIER_URL=${SEPOLIA_VERIFIER_URL} -deploy-swap-facility-sepolia: deploy-swap-facility - -deploy-swap-facility-arbitrum-sepolia: RPC_URL=$(ARBITRUM_SEPOLIA_RPC_URL) -deploy-swap-facility-arbitrum-sepolia: VERIFIER="etherscan" -deploy-swap-facility-arbitrum-sepolia: VERIFIER_URL=${ARBITRUM_SEPOLIA_VERIFIER_URL} -deploy-swap-facility-arbitrum-sepolia: deploy-swap-facility - -deploy-swap-facility-optimism-sepolia: RPC_URL=$(OPTIMISM_SEPOLIA_RPC_URL) -deploy-swap-facility-optimism-sepolia: VERIFIER="etherscan" -deploy-swap-facility-optimism-sepolia: VERIFIER_URL=${OPTIMISM_SEPOLIA_VERIFIER_URL} -deploy-swap-facility-optimism-sepolia: deploy-swap-facility - -deploy-swap-facility-apechain-testnet: RPC_URL=$(APECHAIN_TESTNET_RPC_URL) -deploy-swap-facility-apechain-testnet: VERIFIER="etherscan" -deploy-swap-facility-apechain-testnet: VERIFIER_URL=${APECHAIN_TESTNET_VERIFIER_URL} -deploy-swap-facility-apechain-testnet: deploy-swap-facility - -deploy-swap-facility-bsc-testnet: RPC_URL=$(BSC_TESTNET_RPC_URL) -deploy-swap-facility-bsc-testnet: VERIFIER="etherscan" -deploy-swap-facility-bsc-testnet: VERIFIER_URL=${BSC_TESTNET_VERIFIER_URL} -deploy-swap-facility-bsc-testnet: deploy-swap-facility - -deploy-swap-facility-soneium-testnet: RPC_URL=$(SONEIUM_TESTNET_RPC_URL) -deploy-swap-facility-soneium-testnet: VERIFIER="blockscout" -deploy-swap-facility-soneium-testnet: VERIFIER_URL=${SONEIUM_TESTNET_VERIFIER_URL} -deploy-swap-facility-soneium-testnet: deploy-swap-facility - -deploy-swap-facility-base-sepolia: RPC_URL=$(BASE_SEPOLIA_RPC_URL) -deploy-swap-facility-base-sepolia: VERIFIER="etherscan" -deploy-swap-facility-base-sepolia: VERIFIER_URL=${BASE_SEPOLIA_VERIFIER_URL} -deploy-swap-facility-base-sepolia: deploy-swap-facility - -deploy-swap-facility-base: RPC_URL=$(BASE_RPC_URL) -deploy-swap-facility-base: VERIFIER="etherscan" -deploy-swap-facility-base: VERIFIER_URL=${BASE_VERIFIER_URL} -deploy-swap-facility-base: deploy-swap-facility - -deploy-swap-facility-soneium: RPC_URL=$(SONEIUM_RPC_URL) -deploy-swap-facility-soneium: VERIFIER="blockscout" -deploy-swap-facility-soneium: VERIFIER_URL=${SONEIUM_VERIFIER_URL} -deploy-swap-facility-soneium: deploy-swap-facility - -deploy-swap-facility-plasma: RPC_URL=$(PLASMA_RPC_URL) -deploy-swap-facility-plasma: VERIFIER="custom" -deploy-swap-facility-plasma: VERIFIER_URL=${PLASMA_VERIFIER_URL} -deploy-swap-facility-plasma: deploy-swap-facility - -deploy-swap-facility-citrea: RPC_URL=$(CITREA_RPC_URL) -deploy-swap-facility-citrea: VERIFIER="custom" -deploy-swap-facility-citrea: VERIFIER_URL=${CITREA_VERIFIER_URL} -deploy-swap-facility-citrea: deploy-swap-facility - -deploy-swap-facility-sei: RPC_URL=$(SEI_RPC_URL) -deploy-swap-facility-sei: VERIFIER="etherscan" -deploy-swap-facility-sei: VERIFIER_URL=${SEI_VERIFIER_URL} -deploy-swap-facility-sei: deploy-swap-facility - -deploy-swap-facility-0g: RPC_URL=$(ZG_RPC_URL) -deploy-swap-facility-0g: VERIFIER="custom" -deploy-swap-facility-0g: VERIFIER_URL=$(ZG_VERIFIER_URL) -deploy-swap-facility-0g: deploy-swap-facility - -# -# -# UPGRADE -# -# - -upgrade-swap-facility: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) PAUSER=$(PAUSER) \ - forge script script/upgrade/UpgradeSwapFacility.s.sol:UpgradeSwapFacility \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) \ - --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} - -upgrade-swap-facility-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -upgrade-swap-facility-sepolia: VERIFIER="etherscan" -upgrade-swap-facility-sepolia: VERIFIER_URL=${SEPOLIA_VERIFIER_URL} -upgrade-swap-facility-sepolia: upgrade-swap-facility - -upgrade-swap-facility-arbitrum-sepolia: RPC_URL=$(ARBITRUM_SEPOLIA_RPC_URL) -upgrade-swap-facility-arbitrum-sepolia: VERIFIER="etherscan" -upgrade-swap-facility-arbitrum-sepolia: VERIFIER_URL=${ARBITRUM_SEPOLIA_VERIFIER_URL} -upgrade-swap-facility-arbitrum-sepolia: upgrade-swap-facility - -# This upgrade is strictly specific to Sepolia as it caters to an old SwapFacility deployment -upgrade-old-swap-facility: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) \ - forge script script/upgrade/UpgradeOldSwapFacility.s.sol:UpgradeOldSwapFacility \ - --rpc-url $(SEPOLIA_RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) +# Seismic testnet: broadcast only, then auto-verify from the broadcast. +deploy-jmi-extension-seismic-testnet: RPC_URL=$(SEISMIC_TESTNET_RPC_URL) +deploy-jmi-extension-seismic-testnet: DEPLOY_FLAGS=$(BROADCAST_ONLY_FLAGS) +deploy-jmi-extension-seismic-testnet: POST_DEPLOY=$(call SEISMIC_VERIFY_TESTNET,DeployJMIExtension.s.sol,$(SEISMIC_TESTNET_CHAIN_ID)) +deploy-jmi-extension-seismic-testnet: deploy-jmi-extension -upgrade-jmi-extension: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_ADDRESS=$(EXTENSION_ADDRESS) \ - forge script script/upgrade/UpgradeJMIExtension.s.sol:UpgradeJMIExtension \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) +# Re-verify the latest seismic-testnet broadcast without redeploying. +verify-jmi-extension-seismic-testnet: + $(call SEISMIC_VERIFY_TESTNET,DeployJMIExtension.s.sol,$(SEISMIC_TESTNET_CHAIN_ID)) -upgrade-jmi-extension-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -upgrade-jmi-extension-sepolia: upgrade-jmi-extension +# --- OPS (post-deploy configuration) -------------------------------------------- -# -# -# PROPOSE (via Multisig) -# -# +# Contract-key install via `scast send --seismic` — a forge broadcast would publish the key in plaintext calldata (see script header). Env: EXTENSION_PROXY. +set-contract-key-seismic-testnet: + RPC_URL=$(SEISMIC_TESTNET_RPC_URL) EXTENSION_PROXY=$(EXTENSION_PROXY) PRIVATE_KEY=$(PRIVATE_KEY) ./script/set-contract-key.sh -propose-transfer-swap-facility-owner: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) \ - forge script script/ProposeTransferSwapFacilityOwner.s.sol:ProposeTransferSwapFacilityOwner \ - --rpc-url $(RPC_URL) \ +# Approve the extension on SwapFacility + populate the infra allowlist. Env: EXTENSION_PROXY, PORTAL, LIMIT_ORDER_PROTOCOL. +configure-extension-seismic-testnet: + $(require-toolchain) + FOUNDRY_PROFILE=seismic PRIVATE_KEY=$(PRIVATE_KEY) EXTENSION_PROXY=$(EXTENSION_PROXY) PORTAL=$(PORTAL) LIMIT_ORDER_PROTOCOL=$(LIMIT_ORDER_PROTOCOL) \ + sforge script script/ConfigureSeismicExtension.s.sol:ConfigureSeismicExtension \ + --rpc-url $(SEISMIC_TESTNET_RPC_URL) \ --private-key $(PRIVATE_KEY) \ --skip test --slow --non-interactive $(BROADCAST_ONLY_FLAGS) - -propose-transfer-swap-facility-owner-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -propose-transfer-swap-facility-owner-sepolia: propose-transfer-swap-facility-owner - -propose-transfer-swap-facility-owner-mainnet: RPC_URL=$(MAINNET_RPC_URL) -propose-transfer-swap-facility-owner-mainnet: propose-transfer-swap-facility-owner - -propose-transfer-swap-facility-owner-bsc: RPC_URL=$(BSC_RPC_URL) -propose-transfer-swap-facility-owner-bsc: propose-transfer-swap-facility-owner - -propose-transfer-swap-facility-owner-linea: RPC_URL=$(LINEA_RPC_URL) -propose-transfer-swap-facility-owner-linea: propose-transfer-swap-facility-owner - -propose-swap-facility-upgrade: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) SAFE_ADDRESS=$(SAFE_ADDRESS) PAUSER=$(PAUSER) \ - forge script script/upgrade/ProposeSwapFacilityUpgrade.s.sol:ProposeSwapFacilityUpgrade \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) \ - --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} - -propose-swap-facility-upgrade-sepolia: RPC_URL=$(SEPOLIA_RPC_URL) -propose-swap-facility-upgrade-sepolia: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-mainnet: RPC_URL=$(MAINNET_RPC_URL) -propose-swap-facility-upgrade-mainnet: VERIFIER="etherscan" -propose-swap-facility-upgrade-mainnet: VERIFIER_URL=$(MAINNET_VERIFIER_URL) -propose-swap-facility-upgrade-mainnet: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-arbitrum: RPC_URL=$(ARBITRUM_RPC_URL) -propose-swap-facility-upgrade-arbitrum: VERIFIER="etherscan" -propose-swap-facility-upgrade-arbitrum: VERIFIER_URL=$(ARBITRUM_VERIFIER_URL) -propose-swap-facility-upgrade-arbitrum: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-base: RPC_URL=$(BASE_RPC_URL) -propose-swap-facility-upgrade-base: VERIFIER="etherscan" -propose-swap-facility-upgrade-base: VERIFIER_URL=$(BASE_VERIFIER_URL) -propose-swap-facility-upgrade-base: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-optimism: RPC_URL=$(OPTIMISM_RPC_URL) -propose-swap-facility-upgrade-optimism: VERIFIER="etherscan" -propose-swap-facility-upgrade-optimism: VERIFIER_URL=$(OPTIMISM_VERIFIER_URL) -propose-swap-facility-upgrade-optimism: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-bsc: RPC_URL=$(BSC_RPC_URL) -propose-swap-facility-upgrade-bsc: VERIFIER="etherscan" -propose-swap-facility-upgrade-bsc: VERIFIER_URL=$(BSC_VERIFIER_URL) -propose-swap-facility-upgrade-bsc: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-soneium: RPC_URL=$(SONEIUM_RPC_URL) -propose-swap-facility-upgrade-soneium: VERIFIER="blockscout" -propose-swap-facility-upgrade-soneium: VERIFIER_URL=$(SONEIUM_VERIFIER_URL) -propose-swap-facility-upgrade-soneium: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-mantra: RPC_URL=$(MANTRA_RPC_URL) -propose-swap-facility-upgrade-mantra: VERIFIER="blockscout" -propose-swap-facility-upgrade-mantra: VERIFIER_URL=$(MANTRA_VERIFIER_URL) -propose-swap-facility-upgrade-mantra: propose-swap-facility-upgrade - -propose-swap-facility-upgrade-hyperliquid: RPC_URL=$(HYPERLIQUID_RPC_URL) -propose-swap-facility-upgrade-hyperliquid: VERIFIER="etherscan" -propose-swap-facility-upgrade-hyperliquid: VERIFIER_URL=$(HYPERLIQUID_VERIFIER_URL) -propose-swap-facility-upgrade-hyperliquid: propose-swap-facility-upgrade -# -# -# PROPOSE (via Multisig to Timelock) -# -# - -propose-timelock-swap-facility-upgrade: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) SAFE_ADDRESS=$(SAFE_ADDRESS) TIMELOCK_ADDRESS=$(TIMELOCK_ADDRESS) PAUSER=$(PAUSER) \ - forge script script/upgrade/ProposeTimelockSwapFacilityUpgrade.s.sol:ProposeTimelockSwapFacilityUpgrade \ - --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) \ - --verifier ${VERIFIER} --verifier-url ${VERIFIER_URL} - -propose-timelock-swap-facility-upgrade-mainnet: RPC_URL=$(MAINNET_RPC_URL) -propose-timelock-swap-facility-upgrade-mainnet: VERIFIER="etherscan" -propose-timelock-swap-facility-upgrade-mainnet: VERIFIER_URL=$(MAINNET_VERIFIER_URL) -propose-timelock-swap-facility-upgrade-mainnet: propose-timelock-swap-facility-upgrade - -propose-timelock-swap-facility-upgrade-bsc: RPC_URL=$(BSC_RPC_URL) -propose-timelock-swap-facility-upgrade-bsc: VERIFIER="etherscan" -propose-timelock-swap-facility-upgrade-bsc: VERIFIER_URL=$(BSC_VERIFIER_URL) -propose-timelock-swap-facility-upgrade-bsc: propose-timelock-swap-facility-upgrade - -propose-timelock-swap-facility-upgrade-linea: RPC_URL=$(LINEA_RPC_URL) -propose-timelock-swap-facility-upgrade-linea: VERIFIER="etherscan" -propose-timelock-swap-facility-upgrade-linea: VERIFIER_URL=$(LINEA_VERIFIER_URL) -propose-timelock-swap-facility-upgrade-linea: propose-timelock-swap-facility-upgrade - -# -# -# EXECUTE (Timelock) -# -# - -execute-timelock-swap-facility-upgrade: - FOUNDRY_PROFILE=production PRIVATE_KEY=$(PRIVATE_KEY) TIMELOCK_ADDRESS=$(TIMELOCK_ADDRESS) PAUSER=$(PAUSER) NEW_IMPLEMENTATION=$(NEW_IMPLEMENTATION) \ - forge script script/upgrade/ExecuteTimelockSwapFacilityUpgrade.s.sol:ExecuteTimelockSwapFacilityUpgrade \ - --rpc-url $(RPC_URL) \ - --skip test --slow --non-interactive $(BROADCAST_FLAGS) - -execute-timelock-swap-facility-upgrade-mainnet: RPC_URL=$(MAINNET_RPC_URL) -execute-timelock-swap-facility-upgrade-mainnet: execute-timelock-swap-facility-upgrade - -execute-timelock-swap-facility-upgrade-bsc: RPC_URL=$(BSC_RPC_URL) -execute-timelock-swap-facility-upgrade-bsc: execute-timelock-swap-facility-upgrade - -execute-timelock-swap-facility-upgrade-linea: RPC_URL=$(LINEA_RPC_URL) -execute-timelock-swap-facility-upgrade-linea: execute-timelock-swap-facility-upgrade diff --git a/README.md b/README.md index 154de695..9d09aea4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -## $M Extensions Framework +## $M Extensions Framework — Seismic branch + +> **Standalone branch.** This branch exists solely for the Seismic deployment of `MYieldToOne` +> and never merges back to `main`. It builds with Seismic's `sforge`/`ssolc` toolchain — stock +> Foundry cannot compile it (see [Building this branch](#building-this-branch-seismic-toolchain)). +> Audit scope and the ERC-20 deviations this branch introduces are summarized under +> [Audit scope](#audit-scope). **M Extension Framework** is a modular templates of ERC-20 **stablecoin extensions** that wrap the yield-bearing `$M` token into non-rebasing variants for improved composability within DeFi. Each extension manages yield distribution differently and integrates with a central **SwapFacility** contract that acts as the exclusive entry point for wrapping and unwrapping. @@ -10,14 +16,18 @@ All contracts are deployed behind transparent upgradeable proxies (by default). Each extension inherits from the abstract `MExtension` base contract, which defines shared wrapping logic. Only the `SwapFacility` is authorized to call `wrap()` and `unwrap()`. Yield is accrued based on the locked `$M` balance within each extension and minted via dedicated yield claim functions. -#### In-Scope Extensions +On this branch, `MYieldToOne` is rewritten as a **shielded SRC-20** for the Seismic mercury EVM; the other extensions are source-unchanged (modulo pragma) but are recompiled with `ssolc`. See [Audit scope](#audit-scope) for what is in scope. -- **`MYieldToOne`** +- **`MYieldToOne`** (shielded SRC-20 on this branch) - All yield goes to a single configurable `yieldRecipient` - - Includes a blacklist enforced on all user actions + - Balances and allowances are shielded `suint256`; `balanceOf` and `allowance` reads are gated (an account can read its own balance; third-party reads revert) + - Shielded SRC-20 overloads of `transfer` / `approve` / `transferFrom` take `suint256` amounts, keeping them out of public calldata + - Native ERC-20 paths are infra-only: `transferFrom(uint256)` and `approve(uint256)` work only for the immutable `SwapFacility` and an admin-managed infra allowlist (Portal, LimitOrderProtocol); `transfer(uint256)` and both `permit`s always revert + - Encrypted `Transfer` / `Approval` events: amounts are emitted as per-recipient ECDH + AES-GCM ciphertexts (`setContractKey` installs the one-shot contract keypair; holders opt in via `registerPublicKey`) + - Freezing enforced on all user actions - Handles loss of `$M` earner status gracefully -- **`MYieldToOneForcedTransfer`** +- **`MYieldToOneForcedTransfer`** (deployed on Seismic testnet as USDS) - Inherits all functionality from `MYieldToOne` - Adds compliant fund recovery via forced transfers from frozen accounts @@ -48,6 +58,74 @@ Each extension inherits from the abstract `MExtension` base contract, which defi --- +### Building this branch (Seismic toolchain) + +Shielded types (`suint256`, `sbytes32`) require Seismic's `ssolc` compiler fork and the `mercury` EVM revision; stock `forge`/`solc` fail at parse. The deployed Seismic-testnet bytecode is built with the pinned toolchain below; reproduce the audit with exactly these versions: + +| Tool | Version | Commit | +| ----------------------------- | ------------------------------------------ | ------------------------------------------ | +| `sforge` / `scast` / `sanvil` | `1.3.5-v0.2.0` | `6065731fd5a1367603f6adac38f2fa174cbd66b8` | +| `ssolc` | `0.8.31-develop.2026.4.29+commit.cd9163d8` | `cd9163d8d7926fee2e2d3fe1f9609548e0414bf1` | + +`sfoundryup` always fetches the _latest_ `ssolc` release, so confirm the installed `ssolc` matches the pin above after install. The socialscan verifier expects the label `v0.8.31+commit.cd9163d8` (the `-develop` prerelease tag stripped); `script/verify-seismic.py` handles that conversion. + +First-time setup (repo-local install, nothing touches `~`): + +```bash +# 1. Repo-local toolchain env (sforge auto-uses FOUNDRY_PROFILE=seismic) +source scripts/seismic-env.sh + +# 2. Install sfoundryup +curl -L -H "Accept: application/vnd.github.v3.raw" \ + "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash + +# 3. Install the pinned toolchain release (versions + commits in the table above) +sfoundryup -i v0.2.0 +``` + +Then build and test (the seismic profile is the default on this branch): + +```bash +make build +make tests +``` + +Known limitations: + +- **No slither**: crytic-compile cannot ingest mercury/ssolc builds. The last clean static-analysis baseline is the merge-base with `main` (`87a2f42`). +- **Integration tests need a Seismic devnet**: shielded reads use `eth_getFlaggedStorageAt`, which mainnet-fork RPCs do not serve. +- **Verification** goes through `script/verify-seismic.py` (standard-JSON POST to the socialscan explorer API), not `forge verify-contract` — stock forge cannot reproduce mercury builds. + +--- + +### Audit scope + +In scope — the shielded rewrite and what inherits it: + +| Path | Why | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `src/projects/yieldToOne/MYieldToOne.sol` | Shielded `suint256` balances/allowances, gated reads, shielded SRC-20 overloads, infra allowlist, encrypted events | +| `src/projects/yieldToOne/MYieldToOneForcedTransfer.sol` | Forced transfers on shielded balances (deployed on chain 5124 as USDS) | +| `src/projects/yieldToOne/interfaces/IMYieldToOne.sol` | Interface, events (incl. the `Transfer(…,bytes)` overload), errors | +| `src/projects/jmi/JMIExtension.sol` | In the deployable seismic build; inherits the shielded `MYieldToOne` | +| `src/MExtension.sol` | One behavioral line: `_revertIfInsufficientBalance` made `virtual` | +| `lib/common` (`v1.5.1..a1fbf37`) | 12 lines: ERC-20 entry points made `virtual` | + +Everything else in the diff vs `main` is pragma-only (`0.8.26` → `^0.8.26`). Every contract in the seismic build is recompiled with `ssolc`, so the prior audits (stock solc, unshielded) do not cover this bytecode — see [audits/README.md](audits/README.md). + +#### ERC-20 deviations (intended SRC-20 surface) + +| Surface | This token | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `balanceOf` / `allowance` | Gated reads: revert `Unauthorized` for third parties; allowed for the account itself, allowlisted infra, and compliance roles (freeze / forced-transfer managers) | +| `transfer(address,uint256)` | Always reverts `UseShieldedTransfer` — use `transfer(address,suint256)` | +| `transferFrom(address,address,uint256)` | Allowlisted-infra callers only; others revert `UseShieldedTransfer` | +| `approve(address,uint256)` | Allowlisted-infra spenders only; others revert `UseShieldedApprove` | +| `permit` (both overloads) | Always revert `UseShieldedApprove` | +| `Transfer` / `Approval` events | A second `(…,bytes)` shape (distinct topic0) carries the encrypted amount on shielded paths; mint / burn / infra paths stay plaintext `uint256` | + +--- + ### 🔁 SwapFacility The `SwapFacility` contract acts as the **exclusive router** for all wrapping and swapping operations involving `$M` and its extensions. @@ -58,7 +136,7 @@ The `SwapFacility` contract acts as the **exclusive router** for all wrapping an - `swapInM()`, `swapInMWithPermit()` – Accept `$M` and wrap into the selected extension - `swapOutM()` – Unwrap to `$M` (restricted to whitelisted addresses only) -> All actions are subject to the rules defined by each extension (e.g., blacklists, whitelists) +> All actions are subject to the rules defined by each extension (e.g., freeze lists, whitelists) --- @@ -78,6 +156,18 @@ A helper contract that enables token swaps via Uniswap V3. ## Deployment Addresses +### Seismic Testnet (chain 5124) + +USDS ("Seismic Dollar") is an instance of `MYieldToOneForcedTransfer`. + +| Contract | Address | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| USDS Proxy | [0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018](https://seismic-testnet.socialscan.io/address/0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018) | +| USDS Implementation | [0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9](https://seismic-testnet.socialscan.io/address/0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9) | +| USDS ProxyAdmin | [0x3471d21118f19bfdb84591a92c82546c74f2f321](https://seismic-testnet.socialscan.io/address/0x3471d21118f19bfdb84591a92c82546c74f2f321) | +| SwapFacility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://seismic-testnet.socialscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278) | +| M Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://seismic-testnet.socialscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b) | + ### SwapFacility #### Mainnet diff --git a/audits/README.md b/audits/README.md new file mode 100644 index 00000000..8fb1b6a0 --- /dev/null +++ b/audits/README.md @@ -0,0 +1,20 @@ +# Prior Audit Reports — coverage on this branch + +The seven reports in this directory audited `main`'s **unshielded** code, compiled with +**stock solc 0.8.26** (last common ancestor: merge-base `87a2f42`). **None of them cover +the Seismic branch**: not the shielded `MYieldToOne` rewrite (`suint256` +balances/allowances, gated reads, infra allowlist, encrypted events), nor any bytecode +produced by the ssolc compiler fork — including the "unchanged" contracts +(SwapFacility, the other extensions), whose source is identical modulo pragma but whose +deployed bytecode on Seismic is a fresh, unaudited ssolc/mercury compilation. Scope for +the Seismic audit is summarized in the [repo README](../README.md#audit-scope). + +| Report | Auditor | Covers | +| ------------------------------------------------------------------- | --------------------- | -------------------------------------------------- | +| `Certora MExtension Security Assessment Final Report.pdf` | Certora | `main` M Extensions (unshielded, stock solc) | +| `ChainSecurity_M0_M_Extensions_audit.pdf` | ChainSecurity | `main` M Extensions (unshielded, stock solc) | +| `ChainSecurity_M0_M_Extensions_audit_draft.pdf` | ChainSecurity (draft) | `main` M Extensions (unshielded, stock solc) | +| `Guardian Audits M0 Extensions Report Aug 5.pdf` | Guardian Audits | `main` M Extensions (unshielded, stock solc) | +| `GuardianAudits_M0_MExtensions_report.pdf` | Guardian Audits | `main` M Extensions (unshielded, stock solc) | +| `JMI/2025_12_10_Final_M0_Collaborative_Audit_Report_1765332345.pdf` | Collaborative (JMI) | `main` JMIExtension (unshielded, stock solc) | +| `JMI/M0_EVM-M_Extensions_Review_report.pdf` | JMI review | `main` M Extensions / JMI (unshielded, stock solc) | diff --git a/broadcast/ConfigureSeismicExtension.s.sol/5124/run-1781218140012.json b/broadcast/ConfigureSeismicExtension.s.sol/5124/run-1781218140012.json new file mode 100644 index 00000000..67e89d58 --- /dev/null +++ b/broadcast/ConfigureSeismicExtension.s.sol/5124/run-1781218140012.json @@ -0,0 +1,122 @@ +{ + "transactions": [ + { + "hash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "function": "setAdminApprovedExtension(address,bool)", + "arguments": [ + "0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018", + "true" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "gas": "0x131f2", + "value": "0x0", + "input": "0x9d4d470a000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d0180000000000000000000000000000000000000000000000000000000000000001", + "nonce": "0x39", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + }, + { + "hash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "function": "setAllowlisted(address[],bool)", + "arguments": [ + "[0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd]", + "true" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "gas": "0x124db", + "value": "0x0", + "input": "0x34210774000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d925c84b55e4e44a53749ff5f2a5a13f63d128fd", + "nonce": "0x3a", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0xd132", + "logs": [ + { + "address": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "topics": [ + "0x9d4d0b01aa7e4fe9a02187a57e48faf9eaf09d1a084d3ab7a6ca3cd8f2047c14", + "0x000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d018" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0xa2b969c16f071f8cfe76d393d50e7ccd5149f0cfd615b3550f39f89f56ac7176", + "blockNumber": "0x1020799", + "blockTimestamp": "0x19eb8dfd480", + "transactionHash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000001000000000040000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000018000000000000000000000000000400000000000008000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionIndex": "0x0", + "blockHash": "0xa2b969c16f071f8cfe76d393d50e7ccd5149f0cfd615b3550f39f89f56ac7176", + "blockNumber": "0x1020799", + "gasUsed": "0xd132", + "effectiveGasPrice": "0x3d648d87", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "contractAddress": null + }, + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0xd406", + "logs": [ + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xff571df7d74779bb3bc4c418144ed2539441681cec39b558e6639f5faefc0695", + "0x000000000000000000000000d925c84b55e4e44a53749ff5f2a5a13f63d128fd" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0x5669e16c5306215bbeb2027427c9dafaf8a9989496b8ea2ebe6913a4783b6de3", + "blockNumber": "0x10207a1", + "blockTimestamp": "0x19eb8dfdc0d", + "transactionHash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400004000000000000000000000000000000000000000000200000000080000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000001000000000000000", + "transactionHash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionIndex": "0x0", + "blockHash": "0x5669e16c5306215bbeb2027427c9dafaf8a9989496b8ea2ebe6913a4783b6de3", + "blockNumber": "0x10207a1", + "gasUsed": "0xd406", + "effectiveGasPrice": "0x3d648d87", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "contractAddress": null + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1781218140012, + "chain": 5124, + "commit": "55502a2" +} \ No newline at end of file diff --git a/broadcast/ConfigureSeismicExtension.s.sol/5124/run-latest.json b/broadcast/ConfigureSeismicExtension.s.sol/5124/run-latest.json new file mode 100644 index 00000000..67e89d58 --- /dev/null +++ b/broadcast/ConfigureSeismicExtension.s.sol/5124/run-latest.json @@ -0,0 +1,122 @@ +{ + "transactions": [ + { + "hash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "function": "setAdminApprovedExtension(address,bool)", + "arguments": [ + "0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018", + "true" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "gas": "0x131f2", + "value": "0x0", + "input": "0x9d4d470a000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d0180000000000000000000000000000000000000000000000000000000000000001", + "nonce": "0x39", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + }, + { + "hash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "function": "setAllowlisted(address[],bool)", + "arguments": [ + "[0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd]", + "true" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "gas": "0x124db", + "value": "0x0", + "input": "0x34210774000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d925c84b55e4e44a53749ff5f2a5a13f63d128fd", + "nonce": "0x3a", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0xd132", + "logs": [ + { + "address": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "topics": [ + "0x9d4d0b01aa7e4fe9a02187a57e48faf9eaf09d1a084d3ab7a6ca3cd8f2047c14", + "0x000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d018" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0xa2b969c16f071f8cfe76d393d50e7ccd5149f0cfd615b3550f39f89f56ac7176", + "blockNumber": "0x1020799", + "blockTimestamp": "0x19eb8dfd480", + "transactionHash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000001000000000040000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000018000000000000000000000000000400000000000008000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0xa11fd19e8b600c19fcfa16e8570e19fd21811b3a83c65d7be951724e7800297a", + "transactionIndex": "0x0", + "blockHash": "0xa2b969c16f071f8cfe76d393d50e7ccd5149f0cfd615b3550f39f89f56ac7176", + "blockNumber": "0x1020799", + "gasUsed": "0xd132", + "effectiveGasPrice": "0x3d648d87", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb6807116b3b1b321a390594e31ecd6e0076f6278", + "contractAddress": null + }, + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0xd406", + "logs": [ + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xff571df7d74779bb3bc4c418144ed2539441681cec39b558e6639f5faefc0695", + "0x000000000000000000000000d925c84b55e4e44a53749ff5f2a5a13f63d128fd" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0x5669e16c5306215bbeb2027427c9dafaf8a9989496b8ea2ebe6913a4783b6de3", + "blockNumber": "0x10207a1", + "blockTimestamp": "0x19eb8dfdc0d", + "transactionHash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400004000000000000000000000000000000000000000000200000000080000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000001000000000000000", + "transactionHash": "0xc52aab7515cac15c83e680b7d55d671d664db11d2df00522015e07ec04e22fa0", + "transactionIndex": "0x0", + "blockHash": "0x5669e16c5306215bbeb2027427c9dafaf8a9989496b8ea2ebe6913a4783b6de3", + "blockNumber": "0x10207a1", + "gasUsed": "0xd406", + "effectiveGasPrice": "0x3d648d87", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "contractAddress": null + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1781218140012, + "chain": 5124, + "commit": "55502a2" +} \ No newline at end of file diff --git a/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-1780664953484.json b/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-1780664953484.json new file mode 100644 index 00000000..a90df66b --- /dev/null +++ b/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-1780664953484.json @@ -0,0 +1,314 @@ +{ + "transactions": [ + { + "hash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionType": "CREATE", + "contractName": "MYieldToOneForcedTransfer", + "contractAddress": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9", + "function": null, + "arguments": [ + "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", + "0xB6807116b3B1B321a390594e31ECD6e0076f6278" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "gas": "0x585d3f", + "value": "0x0", + "input": "0x60c060405234801561000f575f5ffd5b5060405161513038038061513083398101604081905261002e91610199565b8181818161003a61009d565b6001600160a01b03821660808190526100665760405163b01d5e2b60e01b815260040160405180910390fd5b6001600160a01b03811660a081905261009257604051636880ffc960e11b815260040160405180910390fd5b5050505050506101ca565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0060088154906101000a900460ff16156100ea5760405163f92ee8a960e01b815260040160405180910390fd5b6001600160401b035f8254906101000a90046001600160401b03166001600160401b03161461017b576001600160401b0381600181546001600160401b03938416820291840219161790556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d291610172916001600160401b0391909116815260200190565b60405180910390a15b50565b80516001600160a01b0381168114610194575f5ffd5b919050565b5f5f604083850312156101aa575f5ffd5b6101b38361017e565b91506101c16020840161017e565b90509250929050565b60805160a051614eff6102315f395f81816108eb01528181610f2f015281816119bc0152611fb501525f818161096301528181610d140152818161187c01528181611a7b01528181611cac01528181612220015281816127160152612fef0152614eff5ff3fe608060405234801561000f575f5ffd5b50600436106104a9575f3560e01c80638562359411610277578063b10c99b511610162578063d505accf116100dd578063e3ee160e11610093578063e63ab1e911610079578063e63ab1e914610a57578063e94a010214610a7e578063ef55bec614610a91575f5ffd5b8063e3ee160e14610a31578063e583983614610a44575f5ffd5b8063d7a49f0b116100c3578063d7a49f0b146109e4578063d9169487146109f7578063dd62ed3e14610a1e575f5ffd5b8063d505accf146109c3578063d547741f146109d1575f5ffd5b8063c9144ddb11610132578063c967891a11610118578063c967891a146109a0578063cc4c5b64146109a8578063cf092995146109b0575f5ffd5b8063c9144ddb14610985578063c91f0c531461098d575f5ffd5b8063b10c99b514610925578063b7b7289914610938578063bf376c7a1461094b578063c3b6f9391461095e575f5ffd5b80639fd5a6cf116101f2578063a8afc01f116101c2578063aad12029116101a8578063aad12029146108c0578063ace150a5146108d3578063ae06b7e4146108e6575f5ffd5b8063a8afc01f146108a5578063a9059cbb146108ad575f5ffd5b80639fd5a6cf14610851578063a08cb48b14610864578063a0cc6a6814610877578063a217fddf1461089e575f5ffd5b806391d14854116102475780639552331e1161022d5780639552331e1461082357806395d89b41146108365780639aa541811461083e575f5ffd5b806391d14854146107fd57806394f5a66e14610810575f5ffd5b806385623594146107bc57806388b7ab63146107cf5780638c6a6767146107e25780638d1fdf2f146107ea575f5ffd5b806336568abe116103975780635b4b03e81161031257806370a08231116102e25780637f2eecc3116102c85780637f2eecc3146107725780638456cb591461079957806384b0196e146107a1575f5ffd5b806370a082311461074c5780637ecebe001461075f575f5ffd5b80635b4b03e8146106f75780635c975abb1461070a5780635e8af8d21461071257806363f1564914610725575f5ffd5b80634259dff91161036757806345cf012d1161034d57806345cf012d146106be578063532992c5146106d15780635a049a70146106e4575f5ffd5b80634259dff91461068457806345c8b1a6146106ab575f5ffd5b806336568abe1461064e57806339f47693146106615780633f4ba83a14610674578063406cf2291461067c575f5ffd5b8063285939841161042757806330adf81f116103f757806333bebb77116103dd57806333bebb771461062057806334210774146106335780633644e51514610646575f5ffd5b806330adf81f146105df578063313ce56714610606575f5ffd5b8063285939841461058a5780632cfd442d146105925780632e62b8c8146105b95780632f2ff15d146105cc575f5ffd5b8063170e20701161047c57806323b872dd1161046257806323b872dd1461053b578063248a9ca31461054e57806326987b6014610561575f5ffd5b8063170e20701461051057806318160ddd14610525575f5ffd5b806301ffc9a7146104ad57806305a3b809146104d557806306fdde03146104e8578063095ea7b3146104fd575b5f5ffd5b6104c06104bb3660046140af565b610aa4565b60405190151581526020015b60405180910390f35b6104c06104e33660046140f5565b610ada565b6104f0610b17565b6040516104cc919061413e565b6104c061050b366004614150565b610bca565b61052361051e3660046141bb565b610c05565b005b61052d610c86565b6040519081526020016104cc565b6104c06105493660046141fa565b610c9b565b61052d61055c366004614238565b610cd8565b610569610d11565b6040516fffffffffffffffffffffffffffffffff90911681526020016104cc565b61052d610d97565b61052d7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c81565b6104c06105c7366004614150565b610dc7565b6105236105da36600461424f565b610dd3565b61052d7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b61060e610df5565b60405160ff90911681526020016104cc565b61052361062e3660046141fa565b610e28565b61052361064136600461428a565b610e5d565b61052d610ea9565b61052361065c36600461424f565b610eec565b61052361066f366004614150565b610f24565b610523610fda565b61052d61100f565b61052d7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca81565b6105236106b93660046140f5565b611020565b6105236106cc3660046140f5565b611061565b6105236106df3660046142dd565b611094565b6105236106f2366004614325565b6110b3565b6105236107053660046143af565b6110d3565b6104c06111a4565b6104f06107203660046140f5565b6111d9565b61052d7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3581565b61052d61075a3660046140f5565b6112a0565b61052d61076d3660046140f5565b61130d565b61052d7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b61052361134a565b6107a961137c565b6040516104cc97969594939291906143f7565b6105236107ca36600461448d565b61149f565b6105236107dd366004614565565b611527565b6104f0611556565b6105236107f83660046140f5565b611584565b6104c061080b36600461424f565b6115c5565b61052361081e3660046145e8565b61161b565b6104c06108313660046141fa565b611768565b6104f0611776565b61052361084c3660046146b6565b61179e565b61052361085f3660046146e2565b6117b2565b610523610872366004614753565b6117cb565b61052d7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b61052d5f81565b6105236117f5565b6104c06108bb366004614150565b6118d7565b6105236108ce3660046141bb565b6118f1565b6105236108e1366004614753565b61196b565b61090d7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016104cc565b6104c0610933366004614150565b61198b565b6105236109463660046147b9565b611999565b610523610959366004614150565b6119b1565b61090d7f000000000000000000000000000000000000000000000000000000000000000081565b6104c0611a64565b61052361099b36600461480e565b611aec565b610523611c37565b61090d611d02565b6105236109be366004614565565b611d3b565b61052361085f3660046148cf565b6105236109df36600461424f565b611d5b565b6105236109f2366004614939565b611d77565b61052d7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b61052d610a2c3660046149d8565b611e5b565b610523610a3f366004614a04565b611ee5565b6104c0610a523660046140f5565b611f05565b61052d7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b6104c0610a8c366004614150565b611f3c565b610523610a9f366004614a04565b611f92565b5f6001600160e01b03198216637965db0b60e01b1480610ad457506301ffc9a760e01b6001600160e01b03198316145b92915050565b5f5f516020614e645f395f51905f525b6001600160a01b0383165f9081526004919091016020526040812054906101000a900460ff169050919050565b60607f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1005b6002018054610b4990614a83565b80601f0160208091040260200160405190810160405280929190818152602001828054610b7590614a83565b8015610bc05780601f10610b9757610100808354040283529160200191610bc0565b820191905f5260205f20905b815481529060010190602001808311610ba357829003601f168201915b5050505050905090565b5f610bd483611fb2565b610bf157604051636492ba0560e11b815260040160405180910390fd5b610bfc338484612001565b50600192915050565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b35610c2f8161208e565b5f516020614e845f395f51905f525f5b83811015610c7f57610c7782868684818110610c5d57610c5d614abb565b9050602002016020810190610c7291906140f5565b612098565b600101610c3f565b5050505050565b5f5f516020614e645f395f51905f5254905090565b5f610ca533611fb2565b610cc25760405163047f414d60e01b815260040160405180910390fd5b610cce8484845f612133565b5060019392505050565b5f8181527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602081905260408220600101549392505050565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166326987b606040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d6e573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d929190614acf565b905090565b5f5f610da2306121ff565b90505f610dad610c86565b9050808211610dbc575f610dc0565b8082035b9250505090565b5f610bfc338484612001565b610ddc82610cd8565b610de58161208e565b610def838361228b565b50505050565b5f807fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da10154906101000a900460ff16905090565b7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c610e528161208e565b610def848484612349565b5f610e678161208e565b5f5b83811015610c7f57610ea1858583818110610e8657610e86614abb565b9050602002016020810190610e9b91906140f5565b84612465565b600101610e69565b5f7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10080544614610ee057610edb612564565b610ee6565b80600101545b91505090565b6001600160a01b0381163314610f155760405163334bd91960e11b815260040160405180910390fd5b610f1f828261261f565b505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610f6d57604051630aff86d760e21b815260040160405180910390fd5b610fd6336001600160a01b031663d737d0c76040518163ffffffff1660e01b8152600401602060405180830381865afa158015610fac573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610fd09190614afe565b826126d3565b5050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a6110048161208e565b61100c61278a565b50565b5f61101861280e565b610d92612834565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3561104a8161208e565b610fd65f516020614e845f395f51905f5283612098565b7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca61108b8161208e565b610fd682612897565b6110a9846110a28686612969565b84846129d7565b610def84846129eb565b6110c9856110c18787612969565b858585612a85565b610c7f85856129eb565b5f6110dd8161208e565b602182146110fe576040516318dca5e960e21b815260040160405180910390fd5b7fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af107b05f516020614e645f395f51905f52901561114d57604051633254a4d960e21b815260040160405180910390fd5b600781018590b160068101611163848683614b60565b507fb05f9a05005addeea16b06e6fb6b4d9ae47f2ebbfc32758a1cf9c61e76452b488484604051611195929190614c1a565b60405180910390a15050505050565b5f7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300818154906101000a900460ff1691505090565b6001600160a01b0381165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10560205260409020606090805461121d90614a83565b80601f016020809104026020016040519081016040528092919081815260200182805461124990614a83565b80156112945780601f1061126b57610100808354040283529160200191611294565b820191905f5260205f20905b81548152906001019060200180831161127757829003601f168201915b50505050509050919050565b5f336001600160a01b038316148015906112c057506112be33611fb2565b155b156112dd576040516282b42960e81b815260040160405180910390fd5b5f516020614e645f395f51905f525b6001600160a01b039092165f9081526002929092016020525060409020b090565b6001600160a01b0381165f9081527f1b21ba3f0a2135d61c468900b54084f04af8111bce0f8bbb6ab8c46d11afbd00602052604081205492915050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a6113748161208e565b61100c612a95565b5f606080828080837f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10060020146305f806040519080825280602002602001820160405280156113d5578160200160208202803683370190505b50600f60f81b94939291908480546113ec90614a83565b80601f016020809104026020016040519081016040528092919081815260200182805461141890614a83565b80156114635780601f1061143a57610100808354040283529160200191611463565b820191905f5260205f20905b81548152906001019060200180831161144657829003601f168201915b50505050509450604051806040016040528060018152602001603160f81b81525093929190965096509650965096509650965090919293949596565b602181146114c0576040516318dca5e960e21b815260040160405180910390fd5b335f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af105602052604090206114f8828483614b60565b5060405133907ffa3256bc03b03ee751ee91c53b7318d6e4321ab4d09757c08a50a3f65f678300905f90a25050565b61153f87611539898989898989612afb565b83612b75565b61154d878787878787612bc0565b50505050505050565b60607fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1068054610b4990614a83565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b356115ae8161208e565b610fd65f516020614e845f395f51905f5283612c08565b5f8281527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602081815260408084206001600160a01b03861685529091528220829054906101000a900460ff1691505092915050565b5f611624612c9a565b90505f6008825460ff6101009290920a9004161590505f80835467ffffffffffffffff6101009290920a90041690505f8115801561165f5750825b90505f8267ffffffffffffffff16600114801561167b5750303b155b905081158015611689575080155b156116a75760405163f92ee8a960e01b815260040160405180910390fd5b6001858181548167ffffffffffffffff021916908367ffffffffffffffff16021790555083156116f0576001856801000000000000000081548160ff0219169083151502179055505b6117008d8d8d8d8d8d8d8d612cc2565b8315611759575f8568010000000000000000815460ff8202191692151502919091179055604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050505050505050565b5f610cce8484846001612133565b60607fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da100610b3b565b5f6117a88161208e565b610f1f8383612465565b604051636492ba0560e11b815260040160405180910390fd5b6117dd886110a28a8a8a8a8a8a612afb565b6117eb888888888888612bc0565b5050505050505050565b6117fd611a64565b61181a5760405163b019ea3560e01b815260040160405180910390fd5b7fee580fdb4da10ea17aa673e6f5c8c2370b4166d6a94bc88900e5a96d0589e3ce611843610d11565b6040516fffffffffffffffffffffffffffffffff909116815260200160405180910390a160405163204e66f960e21b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906381399be4906024015f604051808303815f87803b1580156118c5575f5ffd5b505af1158015610def573d5f5f3e3d5ffd5b5f60405163047f414d60e01b815260040160405180910390fd5b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3561191b8161208e565b5f516020614e845f395f51905f525f5b83811015610c7f576119638286868481811061194957611949614abb565b905060200201602081019061195e91906140f5565b612c08565b60010161192b565b61197d886110a28a8a8a8a8a8a612d09565b6117eb888888888888612d78565b5f610bfc3384846001612e6b565b6119a7836115398585612969565b610f1f83836129eb565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146119fa57604051630aff86d760e21b815260040160405180910390fd5b610fd6336001600160a01b031663d737d0c76040518163ffffffff1660e01b8152600401602060405180830381865afa158015611a39573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611a5d9190614afe565b8383612fb0565b6040516384af270f60e01b81523060048201525f907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906384af270f90602401602060405180830381865afa158015611ac8573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d929190614c48565b5f611af5612c9a565b90505f6008825460ff6101009290920a9004161590505f80835467ffffffffffffffff6101009290920a90041690505f81158015611b305750825b90505f8267ffffffffffffffff166001148015611b4c5750303b155b905081158015611b5a575080155b15611b785760405163f92ee8a960e01b815260040160405180910390fd5b6001858181548167ffffffffffffffff021916908367ffffffffffffffff1602179055508315611bc1576001856801000000000000000081548160ff0219169083151502179055505b611bd08c8c8c8c8c8c8c61306c565b8315611c29575f8568010000000000000000815460ff8202191692151502919091179055604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050505050505050565b611c3f611a64565b15611c5d57604051630f484e6d60e31b815260040160405180910390fd5b7f5098de6eb11dbd1127cf4dcd5e960e3944d48a7570b9b1939cff715cb35c5a18611c86610d11565b6040516fffffffffffffffffffffffffffffffff909116815260200160405180910390a17f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663a36e40fc6040518163ffffffff1660e01b81526004015f604051808303815f87803b1580156118c5575f5ffd5b5f807fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10154906101000a90046001600160a01b0316905090565b611d4d87611539898989898989612d09565b61154d878787878787612d78565b611d6482610cd8565b611d6d8161208e565b610def838361261f565b7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c611da18161208e565b858481141580611db15750808314155b15611dcf5760405163512509d360e11b815260040160405180910390fd5b5f5b81811015611e5057611e48898983818110611dee57611dee614abb565b9050602002016020810190611e0391906140f5565b888884818110611e1557611e15614abb565b9050602002016020810190611e2a91906140f5565b878785818110611e3c57611e3c614abb565b90506020020135612349565b600101611dd1565b505050505050505050565b5f336001600160a01b03841614801590611e7e5750336001600160a01b03831614155b15611e9b576040516282b42960e81b815260040160405180910390fd5b506001600160a01b039182165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af103602090815260408083209390941682529190915220b090565b611ef7896110c18b8b8b8b8b8b612d09565b611e50898989898989612d78565b6001600160a01b0381165f9081525f516020614e845f395f51905f5260205260408120819054906101000a900460ff169050919050565b5f7f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0384165f9081526020918252604080822085835290925290812054906101000a900460ff16905092915050565b611fa4896110c18b8b8b8b8b8b612afb565b611e50898989898989612bc0565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316826001600160a01b03161480610ad457505f516020614e645f395f51905f52610aea565b8061200d84848361311c565b6001600160a01b038481165f8181527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10360209081526040808320948816808452948252918290208690b1815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050565b61100c813361313e565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff166120c2575050565b6001600160a01b0381165f908152602083905260408120600181548160ff021916908315150217905550806001600160a01b03167f07d647ad688e085159820c1d8d030e5765cdc5274d4ee4065c6066b388a2ef594260405161212791815260200190565b60405180910390a25050565b6001600160a01b0384165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af103602090815260408083203384529091529020b05f516020614e645f395f51905f52905f1981146121eb57838110156121c157604051630c95cf2760e11b81523360048201525f6024820152604481018590526064015b60405180910390fd5b6001600160a01b0386165f9081526003830160209081526040808320338452909152902084820390b15b6121f786868686612e6b565b505050505050565b6040516370a0823160e01b81526001600160a01b0382811660048301525f917f0000000000000000000000000000000000000000000000000000000000000000909116906370a0823190602401602060405180830381865afa158015612267573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ad49190614c63565b5f7f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006122b784846115c5565b612340575f848152602082815260408083206001600160a01b038716845290915290206001908181548160ff0219169083151502179055506122f63390565b6001600160a01b0316836001600160a01b0316857f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a46001915050610ad4565b5f915050610ad4565b61235283613177565b61235b826131cd565b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516123a091815260200190565b60405180910390a3336001600160a01b0316826001600160a01b0316846001600160a01b03167f47cea260e2dfb95ed2ab59ad44fe2ac9cddb432afb828d2a1475936b5a2b829a846040516123f791815260200190565b60405180910390a4805f0361240b57505050565b61241583826131ff565b6001600160a01b038084165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af102602052604080822080b085900390b191841681522080b0820190b1505050565b6001600160a01b03821661248c57604051630e23c0c560e21b815260040160405180910390fd5b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af104602052604081205f516020614e645f395f51905f52918315159154906101000a900460ff161515036124ec57505050565b6001600160a01b0383165f90815260048201602052604090208290600181548160ff021916908315150217905550826001600160a01b03167fff571df7d74779bb3bc4c418144ed2539441681cec39b558e6639f5faefc069583604051612557911515815260200190565b60405180910390a2505050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1006002016040516125b79190614c7a565b6040805191829003822060208301939093528101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b5f7f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680061264b84846115c5565b15612340575f848152602082815260408083206001600160a01b03871684529091528120600181548160ff0219169083151502179055506126893390565b6001600160a01b0316836001600160a01b0316857ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b60405160405180910390a46001915050610ad4565b6126dc81613240565b6126e68282613263565b6126f033826131ff565b6126fa3382613282565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af1158015612764573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610f1f9190614c48565b565b612792613321565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033005f81600181548160ff0219169083151502179055507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa6127f03390565b6040516001600160a01b03909116815260200160405180910390a150565b6128166111a4565b156127885760405163d93c066560e01b815260040160405180910390fd5b5f5f61283e610d97565b9050805f0361284e575f91505090565b6040518181527fd1c22369a95f91ae16576036bba6372736ba109f257ad94dccb89e141762e2659060200160405180910390a161289261288c611d02565b82613346565b919050565b6001600160a01b0381166128be5760405163177f500360e21b815260040160405180910390fd5b5f516020614e645f395f51905f525f7fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10154906101000a90046001600160a01b03166001600160a01b0316826001600160a01b03160361291b575050565b8160018083019081546001600160a01b0393841682029184021916179055604051908316907f77f12a3c9f87d4602fe59bb8d2b68c7b516e0cacba414a53e74ea75d435dc18d905f90a25050565b604080517f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960208201526001600160a01b03841691810191909152606081018290525f906129d0906080015b604051602081830303815290604052805190602001206133db565b9392505050565b610def6129e685858585613421565b61345c565b6129f582826135a3565b60017f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0384165f9081526020918252604080822085835290925220600181548160ff02191690831515021790555080826001600160a01b03167f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8160405160405180910390a35050565b610c7f6129e686868686866135dd565b612a9d61280e565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033006001818181548160ff0219169083151502179055507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586127f03390565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208201526001600160a01b038089169282019290925290861660608201526080810185905260a0810184905260c0810183905260e081018290525f90612b6a90610100016129b5565b979650505050505050565b5f612b81848484613625565b90505f816005811115612b9657612b96614cec565b03612ba15750505050565b612bac848484613669565b15612bb75750505050565b610def8161345c565b336001600160a01b03861614612bfa57604051631c5939f360e01b81523360048201526001600160a01b03861660248201526044016121b8565b6121f7868686868686612d78565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff1615612c33575050565b6001600160a01b0381165f9081526020839052604090206001908181548160ff021916908315150217905550806001600160a01b03167f68e0d8c112165d0949ce87205b719ed7d98c7401866c34a159f7c67c6f5620e74260405161212791815260200190565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00610ad4565b612cca613754565b6001600160a01b038116612cf15760405163354368a560e01b815260040160405180910390fd5b612d008888888888888861306c565b6117eb81613779565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208201526001600160a01b038089169282019290925290861660608201526080810185905260a0810184905260c0810183905260e081018290525f90612b6a90610100016129b5565b824211612da1576040516324c7fcd160e11b8152426004820152602481018490526044016121b8565b814210612dca576040516359fe699f60e11b8152426004820152602481018390526044016121b8565b612dd486826135a3565b60017f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0388165f9081526020918252604080822085835290925220600181548160ff02191690831515021790555080866001600160a01b03167f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a560405160405180910390a36121f78686866137d2565b81612e75846131cd565b612e8085858361383f565b8115612e9657612e91858585613869565b612ee4565b836001600160a01b0316856001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051612edb91815260200190565b60405180910390a35b805f03612ef15750610def565b6001600160a01b0385165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10260205260409020b0831115612f605760405163db42144d60e01b81526001600160a01b03861660048201525f6024820152604481018290526064016121b8565b6001600160a01b038086165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af102602052604080822080b085900390b191861681522080b0820190b1610c7f565b612fb9826131cd565b612fc281613240565b612fcd838383613ac0565b6040516323b872dd60e01b8152336004820152306024820152604481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906323b872dd906064016020604051808303815f875af115801561303d573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906130619190614c48565b50610f1f8282613346565b613074613754565b6001600160a01b03821661309b576040516309d50edf60e01b815260040160405180910390fd5b6001600160a01b0384166130c257604051633944ed8760e11b815260040160405180910390fd5b6130cc8787613ac8565b6130d583613adc565b6130de81613b35565b6130e785612897565b6130f15f8561228b565b506117eb7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca8361228b565b5f516020614e845f395f51905f526131348185613b8e565b610def8184613b8e565b61314882826115c5565b610fd65760405163e2517d3f60e01b81526001600160a01b0382166004820152602481018390526044016121b8565b6001600160a01b0381165f9081525f516020614e845f395f51905f526020526040812054906101000a900460ff1661100c5760405163fc78247960e01b81526001600160a01b03821660048201526024016121b8565b6001600160a01b03811661100c57604051630bc2c5df60e11b81526001600160a01b03821660048201526024016121b8565b8061320983613bd9565b1015610fd65760405163db42144d60e01b81526001600160a01b03831660048201525f6024820152604481018290526064016121b8565b805f0361100c576040516377b8dde360e01b8152600481018290526024016121b8565b61326b61280e565b610fd65f516020614e845f395f51905f5283613b8e565b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1026020526040812080b083900390b15f516020614e645f395f51905f5290829082908282540390915550506040518281525f906001600160a01b038516907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906020015b60405180910390a3505050565b6133296111a4565b61278857604051638dfc202b60e01b815260040160405180910390fd5b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1026020526040812080b0830190b15f516020614e645f395f51905f5290829082908282540190915550506040518281526001600160a01b038416905f907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602001613314565b5f6133e4610ea9565b60405161190160f01b6020820152602281019190915260428101839052606201604051602081830303815290604052805190602001209050919050565b5f5f5f61342f868686613bed565b90925090505f82600581111561344757613447614cec565b146134525781612b6a565b612b6a8782613c34565b5f81600581111561346f5761346f614cec565b036134775750565b600181600581111561348b5761348b614cec565b036134a957604051638baa579f60e01b815260040160405180910390fd5b60028160058111156134bd576134bd614cec565b036134db57604051634be6321b60e01b815260040160405180910390fd5b60038160058111156134ef576134ef614cec565b0361350d576040516317e97eb760e31b815260040160405180910390fd5b600481600581111561352157613521614cec565b03613558576040517fff551e8900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600581600581111561356c5761356c614cec565b0361358a576040516310c74b0360e01b815260040160405180910390fd5b604051638baa579f60e01b815260040160405180910390fd5b6135ad8282611f3c565b15610fd65760405163d309466d60e01b81526001600160a01b0383166004820152602481018290526044016121b8565b5f5f5f6135ec87878787613c5d565b90925090505f82600581111561360457613604614cec565b1461360f5781613619565b6136198882613c34565b98975050505050505050565b5f5f5f6136328585613d3f565b90925090505f82600581111561364a5761364a614cec565b14613655578161365f565b61365f8682613c34565b9695505050505050565b5f5f5f856001600160a01b03168585604051602401613689929190614d00565b60408051601f198184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff16630b135d3f60e11b179052516136d39190614d2f565b5f60405180830381855afa9150503d805f811461370b576040519150601f19603f3d011682016040523d82523d5f602084013e613710565b606091505b509150915081801561372457506020815110155b801561365f57508051630b135d3f60e11b906137499083016020908101908401614c63565b149695505050505050565b61375c613d83565b61278857604051631afcd79f60e31b815260040160405180910390fd5b613781613754565b6001600160a01b0381166137a85760405163354368a560e01b815260040160405180910390fd5b610fd67fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c8261228b565b6137db826131cd565b6137e683838361383f565b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161382b91815260200190565b60405180910390a3805f0361240b57505050565b61384761280e565b5f516020614e845f395f51905f5261385f8133613b8e565b6131348185613b8e565b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af105602052604081205f516020614e645f395f51905f52919080546138ba90614a83565b80601f01602080910402602001604051908101604052809291908181526020018280546138e690614a83565b80156139315780601f1061390857610100808354040283529160200191613931565b820191905f5260205f20905b81548152906001019060200180831161391457829003601f168201915b5050505050905080515f036139a057836001600160a01b0316856001600160a01b03167fd7e0e76a339fe8e46c3a779742dd7c08951ec9048bb513b9e5749820fc3a6fb760405180602001604052805f815250604051613991919061413e565b60405180910390a35050505050565b60078201b06139c25760405163cc391d3160e01b815260040160405180910390fd5b5f826008015f81546139d390614d3a565b91905081905590505f6139ea84600701b084613d9f565b90505f6139f682613e5a565b604080516001600160a01b03808c1660208301528a1691810191909152606081018590529091505f906080016040516020818303038152906040528051906020012090505f613a6783838a604051602001613a5391815260200190565b604051602081830303815290604052613f14565b9050886001600160a01b03168a6001600160a01b03167fd7e0e76a339fe8e46c3a779742dd7c08951ec9048bb513b9e5749820fc3a6fb783604051613aac919061413e565b60405180910390a350505050505050505050565b61311c61280e565b613ad0613754565b610fd682826006613fb5565b613ae4613754565b6001600160a01b038116613b0b57604051636cbbd1d360e01b815260040160405180910390fd5b610fd67f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b358261228b565b613b3d613754565b6001600160a01b038116613b645760405163042d717b60e01b815260040160405180910390fd5b610fd67f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a8261228b565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff1615610fd6576040516327951b3f60e11b81526001600160a01b03821660048201526024016121b8565b5f5f516020614e645f395f51905f526112ec565b5f80601b60ff84901c017f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8416613c2687838884613c5d565b935093505050935093915050565b5f816001600160a01b0316836001600160a01b031614613c555760056129d0565b505f92915050565b5f807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0831115613c925750600390505f613d36565b8460ff16601b14158015613caa57508460ff16601c14155b15613cba5750600490505f613d36565b604080515f81526020810180835288905260ff871691810191909152606081018590526080810184905260019060a0016020604051602081039080840390855afa158015613d0a573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811615613d2d575f81613d31565b60015f5b915091505b94509492505050565b5f5f8251604114613d555750600290505f613d7c565b6020830151604084015160608501515f1a9190613d7487848484613c5d565b945094505050505b9250929050565b5f613d8c612c9a565b60089054906101000a900460ff16905090565b5f5f5f60656001600160a01b03168585604051602001613dc0929190614d5e565b60408051601f1981840301815290829052613dda91614d2f565b5f60405180830381855afa9150503d805f8114613e12576040519150601f19603f3d011682016040523d82523d5f602084013e613e17565b606091505b509150915081613e3d576040516375c6821d60e11b8152606560048201526024016121b8565b80806020019051810190613e519190614c63565b95945050505050565b5f5f5f60686001600160a01b031684604051602001613e7b91815260200190565b60408051601f1981840301815290829052613e9591614d2f565b5f60405180830381855afa9150503d805f8114613ecd576040519150601f19603f3d011682016040523d82523d5f602084013e613ed2565b606091505b509150915081613ef8576040516375c6821d60e11b8152606860048201526024016121b8565b80806020019051810190613f0c9190614c63565b949350505050565b60605f5f60666001600160a01b0316868686604051602001613f3893929190614d6f565b60408051601f1981840301815290829052613f5291614d2f565b5f60405180830381855afa9150503d805f8114613f8a576040519150601f19603f3d011682016040523d82523d5f602084013e613f8f565b606091505b509150915081613e51576040516375c6821d60e11b8152606660048201526024016121b8565b613fbd613754565b613fc68361402e565b7fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da100817fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da101600181548160ff021916908360ff16021790555082816002019081610c7f9190614da8565b614036613754565b61100c81614042613754565b61100c8161404e613754565b7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1007f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10261409a8382614da8565b504681556140a6612564565b60019091015550565b5f602082840312156140bf575f5ffd5b81356001600160e01b0319811681146129d0575f5ffd5b6001600160a01b038116811461100c575f5ffd5b8035612892816140d6565b5f60208284031215614105575f5ffd5b81356129d0816140d6565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6129d06020830184614110565b5f5f60408385031215614161575f5ffd5b823561416c816140d6565b946020939093013593505050565b5f5f83601f84011261418a575f5ffd5b50813567ffffffffffffffff8111156141a1575f5ffd5b6020830191508360208260051b8501011115613d7c575f5ffd5b5f5f602083850312156141cc575f5ffd5b823567ffffffffffffffff8111156141e2575f5ffd5b6141ee8582860161417a565b90969095509350505050565b5f5f5f6060848603121561420c575f5ffd5b8335614217816140d6565b92506020840135614227816140d6565b929592945050506040919091013590565b5f60208284031215614248575f5ffd5b5035919050565b5f5f60408385031215614260575f5ffd5b823591506020830135614272816140d6565b809150509250929050565b801515811461100c575f5ffd5b5f5f5f6040848603121561429c575f5ffd5b833567ffffffffffffffff8111156142b2575f5ffd5b6142be8682870161417a565b90945092505060208401356142d28161427d565b809150509250925092565b5f5f5f5f608085870312156142f0575f5ffd5b84356142fb816140d6565b966020860135965060408601359560600135945092505050565b803560ff81168114612892575f5ffd5b5f5f5f5f5f60a08688031215614339575f5ffd5b8535614344816140d6565b94506020860135935061435960408701614315565b94979396509394606081013594506080013592915050565b5f5f83601f840112614381575f5ffd5b50813567ffffffffffffffff811115614398575f5ffd5b602083019150836020828501011115613d7c575f5ffd5b5f5f5f604084860312156143c1575f5ffd5b83359250602084013567ffffffffffffffff8111156143de575f5ffd5b6143ea86828701614371565b9497909650939450505050565b60ff60f81b8816815260e060208201525f61441560e0830189614110565b82810360408401526144278189614110565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b8181101561447c57835183526020938401939092019160010161445e565b50909b9a5050505050505050505050565b5f5f6020838503121561449e575f5ffd5b823567ffffffffffffffff8111156144b4575f5ffd5b6141ee85828601614371565b634e487b7160e01b5f52604160045260245ffd5b5f82601f8301126144e3575f5ffd5b8135602083015f5f67ffffffffffffffff841115614503576145036144c0565b50604051601f19601f85018116603f0116810181811067ffffffffffffffff82111715614532576145326144c0565b604052838152905080828401871015614549575f5ffd5b838360208301375f602085830101528094505050505092915050565b5f5f5f5f5f5f5f60e0888a03121561457b575f5ffd5b8735614586816140d6565b96506020880135614596816140d6565b955060408801359450606088013593506080880135925060a0880135915060c088013567ffffffffffffffff8111156145cd575f5ffd5b6145d98a828b016144d4565b91505092959891949750929550565b5f5f5f5f5f5f5f5f610100898b031215614600575f5ffd5b883567ffffffffffffffff811115614616575f5ffd5b6146228b828c016144d4565b985050602089013567ffffffffffffffff81111561463e575f5ffd5b61464a8b828c016144d4565b975050604089013561465b816140d6565b9550606089013561466b816140d6565b9450608089013561467b816140d6565b935060a089013561468b816140d6565b925061469960c08a016140ea565b91506146a760e08a016140ea565b90509295985092959890939650565b5f5f604083850312156146c7575f5ffd5b82356146d2816140d6565b915060208301356142728161427d565b5f5f5f5f5f60a086880312156146f6575f5ffd5b8535614701816140d6565b94506020860135614711816140d6565b93506040860135925060608601359150608086013567ffffffffffffffff81111561473a575f5ffd5b614746888289016144d4565b9150509295509295909350565b5f5f5f5f5f5f5f5f610100898b03121561476b575f5ffd5b8835614776816140d6565b97506020890135614786816140d6565b979a9799505050506040860135956060810135956080820135955060a0820135945060c0820135935060e0909101359150565b5f5f5f606084860312156147cb575f5ffd5b83356147d6816140d6565b925060208401359150604084013567ffffffffffffffff8111156147f8575f5ffd5b614804868287016144d4565b9150509250925092565b5f5f5f5f5f5f5f60e0888a031215614824575f5ffd5b873567ffffffffffffffff81111561483a575f5ffd5b6148468a828b016144d4565b975050602088013567ffffffffffffffff811115614862575f5ffd5b61486e8a828b016144d4565b965050604088013561487f816140d6565b9450606088013561488f816140d6565b9350608088013561489f816140d6565b925060a08801356148af816140d6565b915060c08801356148bf816140d6565b8091505092959891949750929550565b5f5f5f5f5f5f5f60e0888a0312156148e5575f5ffd5b87356148f0816140d6565b96506020880135614900816140d6565b9550604088013594506060880135935061491c60808901614315565b9699959850939692959460a0840135945060c09093013592915050565b5f5f5f5f5f5f6060878903121561494e575f5ffd5b863567ffffffffffffffff811115614964575f5ffd5b61497089828a0161417a565b909750955050602087013567ffffffffffffffff81111561498f575f5ffd5b61499b89828a0161417a565b909550935050604087013567ffffffffffffffff8111156149ba575f5ffd5b6149c689828a0161417a565b979a9699509497509295939492505050565b5f5f604083850312156149e9575f5ffd5b82356149f4816140d6565b91506020830135614272816140d6565b5f5f5f5f5f5f5f5f5f6101208a8c031215614a1d575f5ffd5b8935614a28816140d6565b985060208a0135614a38816140d6565b975060408a0135965060608a0135955060808a0135945060a08a01359350614a6260c08b01614315565b989b979a50959894979396929550929360e081013593506101000135919050565b600181811c90821680614a9757607f821691505b602082108103614ab557634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b5f60208284031215614adf575f5ffd5b81516fffffffffffffffffffffffffffffffff811681146129d0575f5ffd5b5f60208284031215614b0e575f5ffd5b81516129d0816140d6565b601f821115610f1f57805f5260205f20601f840160051c81016020851015614b3e5750805b601f840160051c820191505b81811015610c7f578054505f8155600101614b4a565b67ffffffffffffffff831115614b7857614b786144c0565b614b8c83614b868354614a83565b83614b19565b5f601f841160018114614bbd575f8515614ba65750838201355b5f19600387901b1c1916600186901b178355610c7f565b5f83815260208120601f198716915b82811015614bec5786850135825560209485019460019092019101614bcc565b5086821015614c08575f1960f88860031b161c19848701351681555b505060018560011b0183555050505050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b5f60208284031215614c58575f5ffd5b81516129d08161427d565b5f60208284031215614c73575f5ffd5b5051919050565b5f5f8354614c8781614a83565b600182168015614c9e5760018114614cb357614ce1565b60ff1983168652811515820286019350614ce1565b865f5260205f205f5b83811015614cd95781548882015260019190910190602001614cbc565b505081860193505b509195945050505050565b634e487b7160e01b5f52602160045260245ffd5b828152604060208201525f613f0c6040830184614110565b5f81518060208401855e5f93019283525090919050565b5f6129d08284614d18565b5f60018201614d5757634e487b7160e01b5f52601160045260245ffd5b5060010190565b8281525f613f0c6020830184614d18565b8381527fffffffffffffffffffffffff0000000000000000000000000000000000000000831660208201525f613e51602c830184614d18565b815167ffffffffffffffff811115614dc257614dc26144c0565b614dd681614dd08454614a83565b84614b19565b6020601f821160018114614e08575f8315614df15750848201515b5f19600385901b1c1916600184901b178455610c7f565b5f84815260208120601f198516915b82811015614e375787850151825560209485019460019092019101614e17565b5084821015614e5457868401515f19600387901b60f8161c191681555b50505050600190811b0190555056feee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1002fd5767309dce890c526ace85d7fe164825199d7dcd99c33588befc51b32ce00a2646970667358221220c0ad0e9be3e6b958ce585abc6b1dccc7bf2f98c7d29985642ffaac818fb2716264736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059000000000000000000000000866a2bf4e572cbcf37d5071a7a58503bfb36be1b000000000000000000000000b6807116b3b1b321a390594e31ecd6e0076f6278", + "nonce": "0x0", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + }, + { + "hash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "function": "deployCreate3(bytes32,bytes)", + "arguments": [ + "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217003f7f0ca20d2a236c987260", + "0x60a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c61720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "gas": "0x15d009", + "value": "0x0", + "input": "0x9c36a28612b1a4226ba7d9ad492779c924b0fc00bdcb6217003f7f0ca20d2a236c987260000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000010f660a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c6172000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000455534453000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x1", + "chainId": "0x1404" + }, + "additionalContracts": [ + { + "transactionType": "CREATE2", + "contractName": null, + "address": "0x4406d3ce5a81f566df5d9120e5abce3a27b19e85", + "initCode": "0x67363d3d37363d34f03d5260086018f3" + }, + { + "transactionType": "CREATE", + "contractName": "TransparentUpgradeableProxy", + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "initCode": "0x60a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c61720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "transactionType": "CREATE", + "contractName": "ProxyAdmin", + "address": "0x3471d21118f19bfdb84591a92c82546c74f2f321", + "initCode": "0x608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e6364393136336438005900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + } + ], + "isFixedGasLimit": false, + "hasShieldedArgs": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x43f8f6", + "logs": [ + { + "address": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "blockHash": "0x7cf0b262a7e5f6037dd69fccf7c17e5c3a6023447b8f1073536ffe2f8fa1eb6c", + "blockNumber": "0xe75a76", + "blockTimestamp": "0x19e97e6e27b", + "transactionHash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000020000000000000000000000000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionIndex": "0x0", + "blockHash": "0x7cf0b262a7e5f6037dd69fccf7c17e5c3a6023447b8f1073536ffe2f8fa1eb6c", + "blockNumber": "0xe75a76", + "gasUsed": "0x43f8f6", + "effectiveGasPrice": "0x8", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": null, + "contractAddress": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0xfcabf", + "logs": [ + { + "address": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "topics": [ + "0x2feea65dd4e9f9cbd86b74b7734210c59a1b2981b5b137bd0ee3e208200c9067", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85", + "0xb14885de9d6f68558ba28817a491949048e11bff2c274a28b8042ca157168233" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d9" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b35", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x2", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x3", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x77f12a3c9f87d4602fe59bb8d2b68c7b516e0cacba414a53e74ea75d435dc18d", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x4", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x5", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x6", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0xc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x7", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x8", + "removed": false + }, + { + "address": "0x3471d21118f19bfdb84591a92c82546c74f2f321", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x9", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003471d21118f19bfdb84591a92c82546c74f2f321", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0xa", + "removed": false + }, + { + "address": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "topics": [ + "0x4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b511", + "0x000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d018" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0xb", + "removed": false + } + ], + "logsBloom": "0x0002000408000000000000200000020040000000000000100880000020000000000000000000000000000000000000000040000000020402000000002000000000000000000000000000008002000200000100000000a000000410000000400000000000228080000000000000000800000000800020000000000000000000400000000000000000000800000000008000010000400080400000004000900000000000000100000020000000000000000000000000000000001000004000000000000020000000000200001000000000002000002404080100002000000020000000800000000200020000000000000800000400000000001000000000000000", + "type": "0x2", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "gasUsed": "0xfcabf", + "effectiveGasPrice": "0x8", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "contractAddress": null + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1780664953484, + "chain": 5124, + "commit": "2cb7a6c" +} diff --git a/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-latest.json b/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-latest.json new file mode 100644 index 00000000..a90df66b --- /dev/null +++ b/broadcast/DeployYieldToOneForcedTransfer.s.sol/5124/run-latest.json @@ -0,0 +1,314 @@ +{ + "transactions": [ + { + "hash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionType": "CREATE", + "contractName": "MYieldToOneForcedTransfer", + "contractAddress": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9", + "function": null, + "arguments": [ + "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", + "0xB6807116b3B1B321a390594e31ECD6e0076f6278" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "gas": "0x585d3f", + "value": "0x0", + "input": "0x60c060405234801561000f575f5ffd5b5060405161513038038061513083398101604081905261002e91610199565b8181818161003a61009d565b6001600160a01b03821660808190526100665760405163b01d5e2b60e01b815260040160405180910390fd5b6001600160a01b03811660a081905261009257604051636880ffc960e11b815260040160405180910390fd5b5050505050506101ca565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0060088154906101000a900460ff16156100ea5760405163f92ee8a960e01b815260040160405180910390fd5b6001600160401b035f8254906101000a90046001600160401b03166001600160401b03161461017b576001600160401b0381600181546001600160401b03938416820291840219161790556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d291610172916001600160401b0391909116815260200190565b60405180910390a15b50565b80516001600160a01b0381168114610194575f5ffd5b919050565b5f5f604083850312156101aa575f5ffd5b6101b38361017e565b91506101c16020840161017e565b90509250929050565b60805160a051614eff6102315f395f81816108eb01528181610f2f015281816119bc0152611fb501525f818161096301528181610d140152818161187c01528181611a7b01528181611cac01528181612220015281816127160152612fef0152614eff5ff3fe608060405234801561000f575f5ffd5b50600436106104a9575f3560e01c80638562359411610277578063b10c99b511610162578063d505accf116100dd578063e3ee160e11610093578063e63ab1e911610079578063e63ab1e914610a57578063e94a010214610a7e578063ef55bec614610a91575f5ffd5b8063e3ee160e14610a31578063e583983614610a44575f5ffd5b8063d7a49f0b116100c3578063d7a49f0b146109e4578063d9169487146109f7578063dd62ed3e14610a1e575f5ffd5b8063d505accf146109c3578063d547741f146109d1575f5ffd5b8063c9144ddb11610132578063c967891a11610118578063c967891a146109a0578063cc4c5b64146109a8578063cf092995146109b0575f5ffd5b8063c9144ddb14610985578063c91f0c531461098d575f5ffd5b8063b10c99b514610925578063b7b7289914610938578063bf376c7a1461094b578063c3b6f9391461095e575f5ffd5b80639fd5a6cf116101f2578063a8afc01f116101c2578063aad12029116101a8578063aad12029146108c0578063ace150a5146108d3578063ae06b7e4146108e6575f5ffd5b8063a8afc01f146108a5578063a9059cbb146108ad575f5ffd5b80639fd5a6cf14610851578063a08cb48b14610864578063a0cc6a6814610877578063a217fddf1461089e575f5ffd5b806391d14854116102475780639552331e1161022d5780639552331e1461082357806395d89b41146108365780639aa541811461083e575f5ffd5b806391d14854146107fd57806394f5a66e14610810575f5ffd5b806385623594146107bc57806388b7ab63146107cf5780638c6a6767146107e25780638d1fdf2f146107ea575f5ffd5b806336568abe116103975780635b4b03e81161031257806370a08231116102e25780637f2eecc3116102c85780637f2eecc3146107725780638456cb591461079957806384b0196e146107a1575f5ffd5b806370a082311461074c5780637ecebe001461075f575f5ffd5b80635b4b03e8146106f75780635c975abb1461070a5780635e8af8d21461071257806363f1564914610725575f5ffd5b80634259dff91161036757806345cf012d1161034d57806345cf012d146106be578063532992c5146106d15780635a049a70146106e4575f5ffd5b80634259dff91461068457806345c8b1a6146106ab575f5ffd5b806336568abe1461064e57806339f47693146106615780633f4ba83a14610674578063406cf2291461067c575f5ffd5b8063285939841161042757806330adf81f116103f757806333bebb77116103dd57806333bebb771461062057806334210774146106335780633644e51514610646575f5ffd5b806330adf81f146105df578063313ce56714610606575f5ffd5b8063285939841461058a5780632cfd442d146105925780632e62b8c8146105b95780632f2ff15d146105cc575f5ffd5b8063170e20701161047c57806323b872dd1161046257806323b872dd1461053b578063248a9ca31461054e57806326987b6014610561575f5ffd5b8063170e20701461051057806318160ddd14610525575f5ffd5b806301ffc9a7146104ad57806305a3b809146104d557806306fdde03146104e8578063095ea7b3146104fd575b5f5ffd5b6104c06104bb3660046140af565b610aa4565b60405190151581526020015b60405180910390f35b6104c06104e33660046140f5565b610ada565b6104f0610b17565b6040516104cc919061413e565b6104c061050b366004614150565b610bca565b61052361051e3660046141bb565b610c05565b005b61052d610c86565b6040519081526020016104cc565b6104c06105493660046141fa565b610c9b565b61052d61055c366004614238565b610cd8565b610569610d11565b6040516fffffffffffffffffffffffffffffffff90911681526020016104cc565b61052d610d97565b61052d7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c81565b6104c06105c7366004614150565b610dc7565b6105236105da36600461424f565b610dd3565b61052d7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b61060e610df5565b60405160ff90911681526020016104cc565b61052361062e3660046141fa565b610e28565b61052361064136600461428a565b610e5d565b61052d610ea9565b61052361065c36600461424f565b610eec565b61052361066f366004614150565b610f24565b610523610fda565b61052d61100f565b61052d7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca81565b6105236106b93660046140f5565b611020565b6105236106cc3660046140f5565b611061565b6105236106df3660046142dd565b611094565b6105236106f2366004614325565b6110b3565b6105236107053660046143af565b6110d3565b6104c06111a4565b6104f06107203660046140f5565b6111d9565b61052d7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3581565b61052d61075a3660046140f5565b6112a0565b61052d61076d3660046140f5565b61130d565b61052d7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b61052361134a565b6107a961137c565b6040516104cc97969594939291906143f7565b6105236107ca36600461448d565b61149f565b6105236107dd366004614565565b611527565b6104f0611556565b6105236107f83660046140f5565b611584565b6104c061080b36600461424f565b6115c5565b61052361081e3660046145e8565b61161b565b6104c06108313660046141fa565b611768565b6104f0611776565b61052361084c3660046146b6565b61179e565b61052361085f3660046146e2565b6117b2565b610523610872366004614753565b6117cb565b61052d7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b61052d5f81565b6105236117f5565b6104c06108bb366004614150565b6118d7565b6105236108ce3660046141bb565b6118f1565b6105236108e1366004614753565b61196b565b61090d7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016104cc565b6104c0610933366004614150565b61198b565b6105236109463660046147b9565b611999565b610523610959366004614150565b6119b1565b61090d7f000000000000000000000000000000000000000000000000000000000000000081565b6104c0611a64565b61052361099b36600461480e565b611aec565b610523611c37565b61090d611d02565b6105236109be366004614565565b611d3b565b61052361085f3660046148cf565b6105236109df36600461424f565b611d5b565b6105236109f2366004614939565b611d77565b61052d7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b61052d610a2c3660046149d8565b611e5b565b610523610a3f366004614a04565b611ee5565b6104c0610a523660046140f5565b611f05565b61052d7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b6104c0610a8c366004614150565b611f3c565b610523610a9f366004614a04565b611f92565b5f6001600160e01b03198216637965db0b60e01b1480610ad457506301ffc9a760e01b6001600160e01b03198316145b92915050565b5f5f516020614e645f395f51905f525b6001600160a01b0383165f9081526004919091016020526040812054906101000a900460ff169050919050565b60607f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1005b6002018054610b4990614a83565b80601f0160208091040260200160405190810160405280929190818152602001828054610b7590614a83565b8015610bc05780601f10610b9757610100808354040283529160200191610bc0565b820191905f5260205f20905b815481529060010190602001808311610ba357829003601f168201915b5050505050905090565b5f610bd483611fb2565b610bf157604051636492ba0560e11b815260040160405180910390fd5b610bfc338484612001565b50600192915050565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b35610c2f8161208e565b5f516020614e845f395f51905f525f5b83811015610c7f57610c7782868684818110610c5d57610c5d614abb565b9050602002016020810190610c7291906140f5565b612098565b600101610c3f565b5050505050565b5f5f516020614e645f395f51905f5254905090565b5f610ca533611fb2565b610cc25760405163047f414d60e01b815260040160405180910390fd5b610cce8484845f612133565b5060019392505050565b5f8181527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602081905260408220600101549392505050565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166326987b606040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d6e573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d929190614acf565b905090565b5f5f610da2306121ff565b90505f610dad610c86565b9050808211610dbc575f610dc0565b8082035b9250505090565b5f610bfc338484612001565b610ddc82610cd8565b610de58161208e565b610def838361228b565b50505050565b5f807fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da10154906101000a900460ff16905090565b7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c610e528161208e565b610def848484612349565b5f610e678161208e565b5f5b83811015610c7f57610ea1858583818110610e8657610e86614abb565b9050602002016020810190610e9b91906140f5565b84612465565b600101610e69565b5f7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10080544614610ee057610edb612564565b610ee6565b80600101545b91505090565b6001600160a01b0381163314610f155760405163334bd91960e11b815260040160405180910390fd5b610f1f828261261f565b505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610f6d57604051630aff86d760e21b815260040160405180910390fd5b610fd6336001600160a01b031663d737d0c76040518163ffffffff1660e01b8152600401602060405180830381865afa158015610fac573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610fd09190614afe565b826126d3565b5050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a6110048161208e565b61100c61278a565b50565b5f61101861280e565b610d92612834565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3561104a8161208e565b610fd65f516020614e845f395f51905f5283612098565b7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca61108b8161208e565b610fd682612897565b6110a9846110a28686612969565b84846129d7565b610def84846129eb565b6110c9856110c18787612969565b858585612a85565b610c7f85856129eb565b5f6110dd8161208e565b602182146110fe576040516318dca5e960e21b815260040160405180910390fd5b7fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af107b05f516020614e645f395f51905f52901561114d57604051633254a4d960e21b815260040160405180910390fd5b600781018590b160068101611163848683614b60565b507fb05f9a05005addeea16b06e6fb6b4d9ae47f2ebbfc32758a1cf9c61e76452b488484604051611195929190614c1a565b60405180910390a15050505050565b5f7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300818154906101000a900460ff1691505090565b6001600160a01b0381165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10560205260409020606090805461121d90614a83565b80601f016020809104026020016040519081016040528092919081815260200182805461124990614a83565b80156112945780601f1061126b57610100808354040283529160200191611294565b820191905f5260205f20905b81548152906001019060200180831161127757829003601f168201915b50505050509050919050565b5f336001600160a01b038316148015906112c057506112be33611fb2565b155b156112dd576040516282b42960e81b815260040160405180910390fd5b5f516020614e645f395f51905f525b6001600160a01b039092165f9081526002929092016020525060409020b090565b6001600160a01b0381165f9081527f1b21ba3f0a2135d61c468900b54084f04af8111bce0f8bbb6ab8c46d11afbd00602052604081205492915050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a6113748161208e565b61100c612a95565b5f606080828080837f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10060020146305f806040519080825280602002602001820160405280156113d5578160200160208202803683370190505b50600f60f81b94939291908480546113ec90614a83565b80601f016020809104026020016040519081016040528092919081815260200182805461141890614a83565b80156114635780601f1061143a57610100808354040283529160200191611463565b820191905f5260205f20905b81548152906001019060200180831161144657829003601f168201915b50505050509450604051806040016040528060018152602001603160f81b81525093929190965096509650965096509650965090919293949596565b602181146114c0576040516318dca5e960e21b815260040160405180910390fd5b335f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af105602052604090206114f8828483614b60565b5060405133907ffa3256bc03b03ee751ee91c53b7318d6e4321ab4d09757c08a50a3f65f678300905f90a25050565b61153f87611539898989898989612afb565b83612b75565b61154d878787878787612bc0565b50505050505050565b60607fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1068054610b4990614a83565b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b356115ae8161208e565b610fd65f516020614e845f395f51905f5283612c08565b5f8281527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602081815260408084206001600160a01b03861685529091528220829054906101000a900460ff1691505092915050565b5f611624612c9a565b90505f6008825460ff6101009290920a9004161590505f80835467ffffffffffffffff6101009290920a90041690505f8115801561165f5750825b90505f8267ffffffffffffffff16600114801561167b5750303b155b905081158015611689575080155b156116a75760405163f92ee8a960e01b815260040160405180910390fd5b6001858181548167ffffffffffffffff021916908367ffffffffffffffff16021790555083156116f0576001856801000000000000000081548160ff0219169083151502179055505b6117008d8d8d8d8d8d8d8d612cc2565b8315611759575f8568010000000000000000815460ff8202191692151502919091179055604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050505050505050565b5f610cce8484846001612133565b60607fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da100610b3b565b5f6117a88161208e565b610f1f8383612465565b604051636492ba0560e11b815260040160405180910390fd5b6117dd886110a28a8a8a8a8a8a612afb565b6117eb888888888888612bc0565b5050505050505050565b6117fd611a64565b61181a5760405163b019ea3560e01b815260040160405180910390fd5b7fee580fdb4da10ea17aa673e6f5c8c2370b4166d6a94bc88900e5a96d0589e3ce611843610d11565b6040516fffffffffffffffffffffffffffffffff909116815260200160405180910390a160405163204e66f960e21b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906381399be4906024015f604051808303815f87803b1580156118c5575f5ffd5b505af1158015610def573d5f5f3e3d5ffd5b5f60405163047f414d60e01b815260040160405180910390fd5b7f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b3561191b8161208e565b5f516020614e845f395f51905f525f5b83811015610c7f576119638286868481811061194957611949614abb565b905060200201602081019061195e91906140f5565b612c08565b60010161192b565b61197d886110a28a8a8a8a8a8a612d09565b6117eb888888888888612d78565b5f610bfc3384846001612e6b565b6119a7836115398585612969565b610f1f83836129eb565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146119fa57604051630aff86d760e21b815260040160405180910390fd5b610fd6336001600160a01b031663d737d0c76040518163ffffffff1660e01b8152600401602060405180830381865afa158015611a39573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611a5d9190614afe565b8383612fb0565b6040516384af270f60e01b81523060048201525f907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906384af270f90602401602060405180830381865afa158015611ac8573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d929190614c48565b5f611af5612c9a565b90505f6008825460ff6101009290920a9004161590505f80835467ffffffffffffffff6101009290920a90041690505f81158015611b305750825b90505f8267ffffffffffffffff166001148015611b4c5750303b155b905081158015611b5a575080155b15611b785760405163f92ee8a960e01b815260040160405180910390fd5b6001858181548167ffffffffffffffff021916908367ffffffffffffffff1602179055508315611bc1576001856801000000000000000081548160ff0219169083151502179055505b611bd08c8c8c8c8c8c8c61306c565b8315611c29575f8568010000000000000000815460ff8202191692151502919091179055604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050505050505050565b611c3f611a64565b15611c5d57604051630f484e6d60e31b815260040160405180910390fd5b7f5098de6eb11dbd1127cf4dcd5e960e3944d48a7570b9b1939cff715cb35c5a18611c86610d11565b6040516fffffffffffffffffffffffffffffffff909116815260200160405180910390a17f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663a36e40fc6040518163ffffffff1660e01b81526004015f604051808303815f87803b1580156118c5575f5ffd5b5f807fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10154906101000a90046001600160a01b0316905090565b611d4d87611539898989898989612d09565b61154d878787878787612d78565b611d6482610cd8565b611d6d8161208e565b610def838361261f565b7fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c611da18161208e565b858481141580611db15750808314155b15611dcf5760405163512509d360e11b815260040160405180910390fd5b5f5b81811015611e5057611e48898983818110611dee57611dee614abb565b9050602002016020810190611e0391906140f5565b888884818110611e1557611e15614abb565b9050602002016020810190611e2a91906140f5565b878785818110611e3c57611e3c614abb565b90506020020135612349565b600101611dd1565b505050505050505050565b5f336001600160a01b03841614801590611e7e5750336001600160a01b03831614155b15611e9b576040516282b42960e81b815260040160405180910390fd5b506001600160a01b039182165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af103602090815260408083209390941682529190915220b090565b611ef7896110c18b8b8b8b8b8b612d09565b611e50898989898989612d78565b6001600160a01b0381165f9081525f516020614e845f395f51905f5260205260408120819054906101000a900460ff169050919050565b5f7f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0384165f9081526020918252604080822085835290925290812054906101000a900460ff16905092915050565b611fa4896110c18b8b8b8b8b8b612afb565b611e50898989898989612bc0565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316826001600160a01b03161480610ad457505f516020614e645f395f51905f52610aea565b8061200d84848361311c565b6001600160a01b038481165f8181527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10360209081526040808320948816808452948252918290208690b1815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050565b61100c813361313e565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff166120c2575050565b6001600160a01b0381165f908152602083905260408120600181548160ff021916908315150217905550806001600160a01b03167f07d647ad688e085159820c1d8d030e5765cdc5274d4ee4065c6066b388a2ef594260405161212791815260200190565b60405180910390a25050565b6001600160a01b0384165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af103602090815260408083203384529091529020b05f516020614e645f395f51905f52905f1981146121eb57838110156121c157604051630c95cf2760e11b81523360048201525f6024820152604481018590526064015b60405180910390fd5b6001600160a01b0386165f9081526003830160209081526040808320338452909152902084820390b15b6121f786868686612e6b565b505050505050565b6040516370a0823160e01b81526001600160a01b0382811660048301525f917f0000000000000000000000000000000000000000000000000000000000000000909116906370a0823190602401602060405180830381865afa158015612267573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ad49190614c63565b5f7f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006122b784846115c5565b612340575f848152602082815260408083206001600160a01b038716845290915290206001908181548160ff0219169083151502179055506122f63390565b6001600160a01b0316836001600160a01b0316857f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a46001915050610ad4565b5f915050610ad4565b61235283613177565b61235b826131cd565b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516123a091815260200190565b60405180910390a3336001600160a01b0316826001600160a01b0316846001600160a01b03167f47cea260e2dfb95ed2ab59ad44fe2ac9cddb432afb828d2a1475936b5a2b829a846040516123f791815260200190565b60405180910390a4805f0361240b57505050565b61241583826131ff565b6001600160a01b038084165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af102602052604080822080b085900390b191841681522080b0820190b1505050565b6001600160a01b03821661248c57604051630e23c0c560e21b815260040160405180910390fd5b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af104602052604081205f516020614e645f395f51905f52918315159154906101000a900460ff161515036124ec57505050565b6001600160a01b0383165f90815260048201602052604090208290600181548160ff021916908315150217905550826001600160a01b03167fff571df7d74779bb3bc4c418144ed2539441681cec39b558e6639f5faefc069583604051612557911515815260200190565b60405180910390a2505050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1006002016040516125b79190614c7a565b6040805191829003822060208301939093528101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b5f7f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680061264b84846115c5565b15612340575f848152602082815260408083206001600160a01b03871684529091528120600181548160ff0219169083151502179055506126893390565b6001600160a01b0316836001600160a01b0316857ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b60405160405180910390a46001915050610ad4565b6126dc81613240565b6126e68282613263565b6126f033826131ff565b6126fa3382613282565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af1158015612764573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610f1f9190614c48565b565b612792613321565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033005f81600181548160ff0219169083151502179055507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa6127f03390565b6040516001600160a01b03909116815260200160405180910390a150565b6128166111a4565b156127885760405163d93c066560e01b815260040160405180910390fd5b5f5f61283e610d97565b9050805f0361284e575f91505090565b6040518181527fd1c22369a95f91ae16576036bba6372736ba109f257ad94dccb89e141762e2659060200160405180910390a161289261288c611d02565b82613346565b919050565b6001600160a01b0381166128be5760405163177f500360e21b815260040160405180910390fd5b5f516020614e645f395f51905f525f7fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10154906101000a90046001600160a01b03166001600160a01b0316826001600160a01b03160361291b575050565b8160018083019081546001600160a01b0393841682029184021916179055604051908316907f77f12a3c9f87d4602fe59bb8d2b68c7b516e0cacba414a53e74ea75d435dc18d905f90a25050565b604080517f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960208201526001600160a01b03841691810191909152606081018290525f906129d0906080015b604051602081830303815290604052805190602001206133db565b9392505050565b610def6129e685858585613421565b61345c565b6129f582826135a3565b60017f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0384165f9081526020918252604080822085835290925220600181548160ff02191690831515021790555080826001600160a01b03167f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8160405160405180910390a35050565b610c7f6129e686868686866135dd565b612a9d61280e565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033006001818181548160ff0219169083151502179055507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586127f03390565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208201526001600160a01b038089169282019290925290861660608201526080810185905260a0810184905260c0810183905260e081018290525f90612b6a90610100016129b5565b979650505050505050565b5f612b81848484613625565b90505f816005811115612b9657612b96614cec565b03612ba15750505050565b612bac848484613669565b15612bb75750505050565b610def8161345c565b336001600160a01b03861614612bfa57604051631c5939f360e01b81523360048201526001600160a01b03861660248201526044016121b8565b6121f7868686868686612d78565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff1615612c33575050565b6001600160a01b0381165f9081526020839052604090206001908181548160ff021916908315150217905550806001600160a01b03167f68e0d8c112165d0949ce87205b719ed7d98c7401866c34a159f7c67c6f5620e74260405161212791815260200190565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00610ad4565b612cca613754565b6001600160a01b038116612cf15760405163354368a560e01b815260040160405180910390fd5b612d008888888888888861306c565b6117eb81613779565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208201526001600160a01b038089169282019290925290861660608201526080810185905260a0810184905260c0810183905260e081018290525f90612b6a90610100016129b5565b824211612da1576040516324c7fcd160e11b8152426004820152602481018490526044016121b8565b814210612dca576040516359fe699f60e11b8152426004820152602481018390526044016121b8565b612dd486826135a3565b60017f1116a1d33aa5fb91b2652b3b0fdb63704173742d6dbecaf4256ebe33a48886006001600160a01b0388165f9081526020918252604080822085835290925220600181548160ff02191690831515021790555080866001600160a01b03167f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a560405160405180910390a36121f78686866137d2565b81612e75846131cd565b612e8085858361383f565b8115612e9657612e91858585613869565b612ee4565b836001600160a01b0316856001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051612edb91815260200190565b60405180910390a35b805f03612ef15750610def565b6001600160a01b0385165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af10260205260409020b0831115612f605760405163db42144d60e01b81526001600160a01b03861660048201525f6024820152604481018290526064016121b8565b6001600160a01b038086165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af102602052604080822080b085900390b191861681522080b0820190b1610c7f565b612fb9826131cd565b612fc281613240565b612fcd838383613ac0565b6040516323b872dd60e01b8152336004820152306024820152604481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906323b872dd906064016020604051808303815f875af115801561303d573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906130619190614c48565b50610f1f8282613346565b613074613754565b6001600160a01b03821661309b576040516309d50edf60e01b815260040160405180910390fd5b6001600160a01b0384166130c257604051633944ed8760e11b815260040160405180910390fd5b6130cc8787613ac8565b6130d583613adc565b6130de81613b35565b6130e785612897565b6130f15f8561228b565b506117eb7f4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca8361228b565b5f516020614e845f395f51905f526131348185613b8e565b610def8184613b8e565b61314882826115c5565b610fd65760405163e2517d3f60e01b81526001600160a01b0382166004820152602481018390526044016121b8565b6001600160a01b0381165f9081525f516020614e845f395f51905f526020526040812054906101000a900460ff1661100c5760405163fc78247960e01b81526001600160a01b03821660048201526024016121b8565b6001600160a01b03811661100c57604051630bc2c5df60e11b81526001600160a01b03821660048201526024016121b8565b8061320983613bd9565b1015610fd65760405163db42144d60e01b81526001600160a01b03831660048201525f6024820152604481018290526064016121b8565b805f0361100c576040516377b8dde360e01b8152600481018290526024016121b8565b61326b61280e565b610fd65f516020614e845f395f51905f5283613b8e565b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1026020526040812080b083900390b15f516020614e645f395f51905f5290829082908282540390915550506040518281525f906001600160a01b038516907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906020015b60405180910390a3505050565b6133296111a4565b61278857604051638dfc202b60e01b815260040160405180910390fd5b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1026020526040812080b0830190b15f516020614e645f395f51905f5290829082908282540190915550506040518281526001600160a01b038416905f907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602001613314565b5f6133e4610ea9565b60405161190160f01b6020820152602281019190915260428101839052606201604051602081830303815290604052805190602001209050919050565b5f5f5f61342f868686613bed565b90925090505f82600581111561344757613447614cec565b146134525781612b6a565b612b6a8782613c34565b5f81600581111561346f5761346f614cec565b036134775750565b600181600581111561348b5761348b614cec565b036134a957604051638baa579f60e01b815260040160405180910390fd5b60028160058111156134bd576134bd614cec565b036134db57604051634be6321b60e01b815260040160405180910390fd5b60038160058111156134ef576134ef614cec565b0361350d576040516317e97eb760e31b815260040160405180910390fd5b600481600581111561352157613521614cec565b03613558576040517fff551e8900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600581600581111561356c5761356c614cec565b0361358a576040516310c74b0360e01b815260040160405180910390fd5b604051638baa579f60e01b815260040160405180910390fd5b6135ad8282611f3c565b15610fd65760405163d309466d60e01b81526001600160a01b0383166004820152602481018290526044016121b8565b5f5f5f6135ec87878787613c5d565b90925090505f82600581111561360457613604614cec565b1461360f5781613619565b6136198882613c34565b98975050505050505050565b5f5f5f6136328585613d3f565b90925090505f82600581111561364a5761364a614cec565b14613655578161365f565b61365f8682613c34565b9695505050505050565b5f5f5f856001600160a01b03168585604051602401613689929190614d00565b60408051601f198184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff16630b135d3f60e11b179052516136d39190614d2f565b5f60405180830381855afa9150503d805f811461370b576040519150601f19603f3d011682016040523d82523d5f602084013e613710565b606091505b509150915081801561372457506020815110155b801561365f57508051630b135d3f60e11b906137499083016020908101908401614c63565b149695505050505050565b61375c613d83565b61278857604051631afcd79f60e31b815260040160405180910390fd5b613781613754565b6001600160a01b0381166137a85760405163354368a560e01b815260040160405180910390fd5b610fd67fc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c8261228b565b6137db826131cd565b6137e683838361383f565b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161382b91815260200190565b60405180910390a3805f0361240b57505050565b61384761280e565b5f516020614e845f395f51905f5261385f8133613b8e565b6131348185613b8e565b6001600160a01b0382165f9081527fee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af105602052604081205f516020614e645f395f51905f52919080546138ba90614a83565b80601f01602080910402602001604051908101604052809291908181526020018280546138e690614a83565b80156139315780601f1061390857610100808354040283529160200191613931565b820191905f5260205f20905b81548152906001019060200180831161391457829003601f168201915b5050505050905080515f036139a057836001600160a01b0316856001600160a01b03167fd7e0e76a339fe8e46c3a779742dd7c08951ec9048bb513b9e5749820fc3a6fb760405180602001604052805f815250604051613991919061413e565b60405180910390a35050505050565b60078201b06139c25760405163cc391d3160e01b815260040160405180910390fd5b5f826008015f81546139d390614d3a565b91905081905590505f6139ea84600701b084613d9f565b90505f6139f682613e5a565b604080516001600160a01b03808c1660208301528a1691810191909152606081018590529091505f906080016040516020818303038152906040528051906020012090505f613a6783838a604051602001613a5391815260200190565b604051602081830303815290604052613f14565b9050886001600160a01b03168a6001600160a01b03167fd7e0e76a339fe8e46c3a779742dd7c08951ec9048bb513b9e5749820fc3a6fb783604051613aac919061413e565b60405180910390a350505050505050505050565b61311c61280e565b613ad0613754565b610fd682826006613fb5565b613ae4613754565b6001600160a01b038116613b0b57604051636cbbd1d360e01b815260040160405180910390fd5b610fd67f109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b358261228b565b613b3d613754565b6001600160a01b038116613b645760405163042d717b60e01b815260040160405180910390fd5b610fd67f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a8261228b565b6001600160a01b0381165f90815260208390526040812054906101000a900460ff1615610fd6576040516327951b3f60e11b81526001600160a01b03821660048201526024016121b8565b5f5f516020614e645f395f51905f526112ec565b5f80601b60ff84901c017f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8416613c2687838884613c5d565b935093505050935093915050565b5f816001600160a01b0316836001600160a01b031614613c555760056129d0565b505f92915050565b5f807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0831115613c925750600390505f613d36565b8460ff16601b14158015613caa57508460ff16601c14155b15613cba5750600490505f613d36565b604080515f81526020810180835288905260ff871691810191909152606081018590526080810184905260019060a0016020604051602081039080840390855afa158015613d0a573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811615613d2d575f81613d31565b60015f5b915091505b94509492505050565b5f5f8251604114613d555750600290505f613d7c565b6020830151604084015160608501515f1a9190613d7487848484613c5d565b945094505050505b9250929050565b5f613d8c612c9a565b60089054906101000a900460ff16905090565b5f5f5f60656001600160a01b03168585604051602001613dc0929190614d5e565b60408051601f1981840301815290829052613dda91614d2f565b5f60405180830381855afa9150503d805f8114613e12576040519150601f19603f3d011682016040523d82523d5f602084013e613e17565b606091505b509150915081613e3d576040516375c6821d60e11b8152606560048201526024016121b8565b80806020019051810190613e519190614c63565b95945050505050565b5f5f5f60686001600160a01b031684604051602001613e7b91815260200190565b60408051601f1981840301815290829052613e9591614d2f565b5f60405180830381855afa9150503d805f8114613ecd576040519150601f19603f3d011682016040523d82523d5f602084013e613ed2565b606091505b509150915081613ef8576040516375c6821d60e11b8152606860048201526024016121b8565b80806020019051810190613f0c9190614c63565b949350505050565b60605f5f60666001600160a01b0316868686604051602001613f3893929190614d6f565b60408051601f1981840301815290829052613f5291614d2f565b5f60405180830381855afa9150503d805f8114613f8a576040519150601f19603f3d011682016040523d82523d5f602084013e613f8f565b606091505b509150915081613e51576040516375c6821d60e11b8152606660048201526024016121b8565b613fbd613754565b613fc68361402e565b7fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da100817fcbbe23efb65c1eaba394256c463812c20abdb5376e247eba1d0e1e92054da101600181548160ff021916908360ff16021790555082816002019081610c7f9190614da8565b614036613754565b61100c81614042613754565b61100c8161404e613754565b7f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea1007f103ce0bed7138196cdb0d79ef04042681b16e7a2c58d74b78443c813042ea10261409a8382614da8565b504681556140a6612564565b60019091015550565b5f602082840312156140bf575f5ffd5b81356001600160e01b0319811681146129d0575f5ffd5b6001600160a01b038116811461100c575f5ffd5b8035612892816140d6565b5f60208284031215614105575f5ffd5b81356129d0816140d6565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6129d06020830184614110565b5f5f60408385031215614161575f5ffd5b823561416c816140d6565b946020939093013593505050565b5f5f83601f84011261418a575f5ffd5b50813567ffffffffffffffff8111156141a1575f5ffd5b6020830191508360208260051b8501011115613d7c575f5ffd5b5f5f602083850312156141cc575f5ffd5b823567ffffffffffffffff8111156141e2575f5ffd5b6141ee8582860161417a565b90969095509350505050565b5f5f5f6060848603121561420c575f5ffd5b8335614217816140d6565b92506020840135614227816140d6565b929592945050506040919091013590565b5f60208284031215614248575f5ffd5b5035919050565b5f5f60408385031215614260575f5ffd5b823591506020830135614272816140d6565b809150509250929050565b801515811461100c575f5ffd5b5f5f5f6040848603121561429c575f5ffd5b833567ffffffffffffffff8111156142b2575f5ffd5b6142be8682870161417a565b90945092505060208401356142d28161427d565b809150509250925092565b5f5f5f5f608085870312156142f0575f5ffd5b84356142fb816140d6565b966020860135965060408601359560600135945092505050565b803560ff81168114612892575f5ffd5b5f5f5f5f5f60a08688031215614339575f5ffd5b8535614344816140d6565b94506020860135935061435960408701614315565b94979396509394606081013594506080013592915050565b5f5f83601f840112614381575f5ffd5b50813567ffffffffffffffff811115614398575f5ffd5b602083019150836020828501011115613d7c575f5ffd5b5f5f5f604084860312156143c1575f5ffd5b83359250602084013567ffffffffffffffff8111156143de575f5ffd5b6143ea86828701614371565b9497909650939450505050565b60ff60f81b8816815260e060208201525f61441560e0830189614110565b82810360408401526144278189614110565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b8181101561447c57835183526020938401939092019160010161445e565b50909b9a5050505050505050505050565b5f5f6020838503121561449e575f5ffd5b823567ffffffffffffffff8111156144b4575f5ffd5b6141ee85828601614371565b634e487b7160e01b5f52604160045260245ffd5b5f82601f8301126144e3575f5ffd5b8135602083015f5f67ffffffffffffffff841115614503576145036144c0565b50604051601f19601f85018116603f0116810181811067ffffffffffffffff82111715614532576145326144c0565b604052838152905080828401871015614549575f5ffd5b838360208301375f602085830101528094505050505092915050565b5f5f5f5f5f5f5f60e0888a03121561457b575f5ffd5b8735614586816140d6565b96506020880135614596816140d6565b955060408801359450606088013593506080880135925060a0880135915060c088013567ffffffffffffffff8111156145cd575f5ffd5b6145d98a828b016144d4565b91505092959891949750929550565b5f5f5f5f5f5f5f5f610100898b031215614600575f5ffd5b883567ffffffffffffffff811115614616575f5ffd5b6146228b828c016144d4565b985050602089013567ffffffffffffffff81111561463e575f5ffd5b61464a8b828c016144d4565b975050604089013561465b816140d6565b9550606089013561466b816140d6565b9450608089013561467b816140d6565b935060a089013561468b816140d6565b925061469960c08a016140ea565b91506146a760e08a016140ea565b90509295985092959890939650565b5f5f604083850312156146c7575f5ffd5b82356146d2816140d6565b915060208301356142728161427d565b5f5f5f5f5f60a086880312156146f6575f5ffd5b8535614701816140d6565b94506020860135614711816140d6565b93506040860135925060608601359150608086013567ffffffffffffffff81111561473a575f5ffd5b614746888289016144d4565b9150509295509295909350565b5f5f5f5f5f5f5f5f610100898b03121561476b575f5ffd5b8835614776816140d6565b97506020890135614786816140d6565b979a9799505050506040860135956060810135956080820135955060a0820135945060c0820135935060e0909101359150565b5f5f5f606084860312156147cb575f5ffd5b83356147d6816140d6565b925060208401359150604084013567ffffffffffffffff8111156147f8575f5ffd5b614804868287016144d4565b9150509250925092565b5f5f5f5f5f5f5f60e0888a031215614824575f5ffd5b873567ffffffffffffffff81111561483a575f5ffd5b6148468a828b016144d4565b975050602088013567ffffffffffffffff811115614862575f5ffd5b61486e8a828b016144d4565b965050604088013561487f816140d6565b9450606088013561488f816140d6565b9350608088013561489f816140d6565b925060a08801356148af816140d6565b915060c08801356148bf816140d6565b8091505092959891949750929550565b5f5f5f5f5f5f5f60e0888a0312156148e5575f5ffd5b87356148f0816140d6565b96506020880135614900816140d6565b9550604088013594506060880135935061491c60808901614315565b9699959850939692959460a0840135945060c09093013592915050565b5f5f5f5f5f5f6060878903121561494e575f5ffd5b863567ffffffffffffffff811115614964575f5ffd5b61497089828a0161417a565b909750955050602087013567ffffffffffffffff81111561498f575f5ffd5b61499b89828a0161417a565b909550935050604087013567ffffffffffffffff8111156149ba575f5ffd5b6149c689828a0161417a565b979a9699509497509295939492505050565b5f5f604083850312156149e9575f5ffd5b82356149f4816140d6565b91506020830135614272816140d6565b5f5f5f5f5f5f5f5f5f6101208a8c031215614a1d575f5ffd5b8935614a28816140d6565b985060208a0135614a38816140d6565b975060408a0135965060608a0135955060808a0135945060a08a01359350614a6260c08b01614315565b989b979a50959894979396929550929360e081013593506101000135919050565b600181811c90821680614a9757607f821691505b602082108103614ab557634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b5f60208284031215614adf575f5ffd5b81516fffffffffffffffffffffffffffffffff811681146129d0575f5ffd5b5f60208284031215614b0e575f5ffd5b81516129d0816140d6565b601f821115610f1f57805f5260205f20601f840160051c81016020851015614b3e5750805b601f840160051c820191505b81811015610c7f578054505f8155600101614b4a565b67ffffffffffffffff831115614b7857614b786144c0565b614b8c83614b868354614a83565b83614b19565b5f601f841160018114614bbd575f8515614ba65750838201355b5f19600387901b1c1916600186901b178355610c7f565b5f83815260208120601f198716915b82811015614bec5786850135825560209485019460019092019101614bcc565b5086821015614c08575f1960f88860031b161c19848701351681555b505060018560011b0183555050505050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b5f60208284031215614c58575f5ffd5b81516129d08161427d565b5f60208284031215614c73575f5ffd5b5051919050565b5f5f8354614c8781614a83565b600182168015614c9e5760018114614cb357614ce1565b60ff1983168652811515820286019350614ce1565b865f5260205f205f5b83811015614cd95781548882015260019190910190602001614cbc565b505081860193505b509195945050505050565b634e487b7160e01b5f52602160045260245ffd5b828152604060208201525f613f0c6040830184614110565b5f81518060208401855e5f93019283525090919050565b5f6129d08284614d18565b5f60018201614d5757634e487b7160e01b5f52601160045260245ffd5b5060010190565b8281525f613f0c6020830184614d18565b8381527fffffffffffffffffffffffff0000000000000000000000000000000000000000831660208201525f613e51602c830184614d18565b815167ffffffffffffffff811115614dc257614dc26144c0565b614dd681614dd08454614a83565b84614b19565b6020601f821160018114614e08575f8315614df15750848201515b5f19600385901b1c1916600184901b178455610c7f565b5f84815260208120601f198516915b82811015614e375787850151825560209485019460019092019101614e17565b5084821015614e5457868401515f19600387901b60f8161c191681555b50505050600190811b0190555056feee2f6fc7e2e5879b17985791e0d12536cba689bda43c77b8911497248f4af1002fd5767309dce890c526ace85d7fe164825199d7dcd99c33588befc51b32ce00a2646970667358221220c0ad0e9be3e6b958ce585abc6b1dccc7bf2f98c7d29985642ffaac818fb2716264736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059000000000000000000000000866a2bf4e572cbcf37d5071a7a58503bfb36be1b000000000000000000000000b6807116b3b1b321a390594e31ecd6e0076f6278", + "nonce": "0x0", + "chainId": "0x1404" + }, + "additionalContracts": [], + "isFixedGasLimit": false, + "hasShieldedArgs": false + }, + { + "hash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "function": "deployCreate3(bytes32,bytes)", + "arguments": [ + "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217003f7f0ca20d2a236c987260", + "0x60a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c61720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "transaction": { + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "gas": "0x15d009", + "value": "0x0", + "input": "0x9c36a28612b1a4226ba7d9ad492779c924b0fc00bdcb6217003f7f0ca20d2a236c987260000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000010f660a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c6172000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000455534453000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x1", + "chainId": "0x1404" + }, + "additionalContracts": [ + { + "transactionType": "CREATE2", + "contractName": null, + "address": "0x4406d3ce5a81f566df5d9120e5abce3a27b19e85", + "initCode": "0x67363d3d37363d34f03d5260086018f3" + }, + { + "transactionType": "CREATE", + "contractName": "TransparentUpgradeableProxy", + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "initCode": "0x60a0604052604051610ed6380380610ed68339810160408190526100229161037c565b828161002e828261008c565b50508160405161003d90610340565b6001600160a01b039091168152602001604051809103905ff080158015610066573d5f5f3e3d5ffd5b506001600160a01b031660805261008461007f60805190565b6100ea565b505050610463565b61009582610141565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156100de576100d982826101c1565b505050565b6100e6610234565b5050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610113610255565b604080516001600160a01b03928316815291841660208301520160405180910390a161013e8161027b565b50565b806001600160a01b03163b5f0361017b57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101dd919061044d565b5f60405180830381855af49150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b50909250905061022b8583836102b8565b95945050505050565b34156102535760405163b398979f60e01b815260040160405180910390fd5b565b5f805f516020610eb65f395f51905f5254906101000a90046001600160a01b0316905090565b6001600160a01b0381166102a457604051633173bdd160e11b81525f6004820152602401610172565b805f516020610eb65f395f51905f5261019e565b6060826102cd576102c882610317565b610310565b81511580156102e457506001600160a01b0384163b155b1561030d57604051639996b31560e01b81526001600160a01b0385166004820152602401610172565b50805b9392505050565b8051156103275780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b61058f8061092783390190565b80516001600160a01b0381168114610363575f5ffd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f5f5f6060848603121561038e575f5ffd5b6103978461034d565b92506103a56020850161034d565b60408501519092506001600160401b038111156103c0575f5ffd5b8401601f810186136103d0575f5ffd5b80516001600160401b038111156103e9576103e9610368565b604051601f8201601f19908116603f011681016001600160401b038111828210171561041757610417610368565b60405281815282820160200188101561042e575f5ffd5b8160208401602083015e5f602083830101528093505050509250925092565b5f82518060208501845e5f920191825250919050565b6080516104ad61047a5f395f601001526104ad5ff3fe608060405261000c61000e565b005b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163303610092575f357fffffffff000000000000000000000000000000000000000000000000000000001663278f794360e11b14610088576040516334ad5dbb60e21b815260040160405180910390fd5b61009061009a565b565b6100906100c8565b5f806100a9366004818461032f565b8101906100b6919061036a565b915091506100c482826100d8565b5050565b6100906100d3610132565b610140565b6100e18261015e565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561012a5761012582826101dd565b505050565b6100c461024f565b5f61013b61026e565b905090565b365f5f375f5f365f845af43d5f5f3e80801561015a573d5ff35b3d5ffd5b806001600160a01b03163b5f0361019857604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60018154816001600160a01b0302191690836001600160a01b0316021790555050565b60605f5f846001600160a01b0316846040516101f9919061043b565b5f60405180830381855af49150503d805f8114610231576040519150601f19603f3d011682016040523d82523d5f602084013e610236565b606091505b50915091506102468583836102a7565b95945050505050565b34156100905760405163b398979f60e01b815260040160405180910390fd5b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54906101000a90046001600160a01b0316905090565b6060826102bc576102b782610306565b6102ff565b81511580156102d357506001600160a01b0384163b155b156102fc57604051639996b31560e01b81526001600160a01b038516600482015260240161018f565b50805b9392505050565b8051156103165780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b5f5f8585111561033d575f5ffd5b83861115610349575f5ffd5b5050820193919092039150565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561037b575f5ffd5b82356001600160a01b0381168114610391575f5ffd5b9150602083013567ffffffffffffffff8111156103ac575f5ffd5b8301601f810185136103bc575f5ffd5b803567ffffffffffffffff8111156103d6576103d6610356565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561040557610405610356565b60405281815282820160200187101561041c575f5ffd5b816020840160208301375f602083830101528093505050509250929050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220ef8754db0058f85d211886776361a0e218965b6cdb1f8b9227d805e2abf0ffc664736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e63643931363364380059b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb62170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018494f5a66e0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb621700000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217000000000000000000000000000000000000000000000000000000000000000e536569736d696320446f6c6c61720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "transactionType": "CREATE", + "contractName": "ProxyAdmin", + "address": "0x3471d21118f19bfdb84591a92c82546c74f2f321", + "initCode": "0x608060405234801561000f575f5ffd5b5060405161058f38038061058f83398101604081905261002e916100d3565b806001600160a01b03811661005c57604051631e4fbdf760e01b81525f600482015260240160405180910390fd5b6100658161006c565b5050610100565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f602082840312156100e3575f5ffd5b81516001600160a01b03811681146100f9575f5ffd5b9392505050565b6104828061010d5f395ff3fe608060405260043610610058575f3560e01c80639623609d116100415780639623609d146100a3578063ad3cb1cc146100b6578063f2fde38b1461010b575f5ffd5b8063715018a61461005c5780638da5cb5b14610072575b5f5ffd5b348015610067575f5ffd5b5061007061012a565b005b34801561007d575f5ffd5b5061008661013d565b6040516001600160a01b0390911681526020015b60405180910390f35b6100706100b13660046102c4565b610156565b3480156100c1575f5ffd5b506100fe6040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161009a91906103c9565b348015610116575f5ffd5b506100706101253660046103e2565b6101c1565b610132610203565b61013b5f610235565b565b5f808054906101000a90046001600160a01b0316905090565b61015e610203565b60405163278f794360e11b81526001600160a01b03841690634f1ef28690349061018e90869086906004016103fd565b5f604051808303818588803b1580156101a5575f5ffd5b505af11580156101b7573d5f5f3e3d5ffd5b5050505050505050565b6101c9610203565b6001600160a01b0381166101f757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61020081610235565b50565b3361020c61013d565b6001600160a01b03161461013b5760405163118cdaa760e01b81523360048201526024016101ee565b5f808054906101000a90046001600160a01b03169050815f5f6101000a81546001600160a01b0393841682029184021916179055604051838216918316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381168114610200575f5ffd5b634e487b7160e01b5f52604160045260245ffd5b5f5f5f606084860312156102d6575f5ffd5b83356102e18161029c565b925060208401356102f18161029c565b9150604084013567ffffffffffffffff81111561030c575f5ffd5b8401601f8101861361031c575f5ffd5b803567ffffffffffffffff811115610336576103366102b0565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610365576103656102b0565b60405281815282820160200188101561037c575f5ffd5b816020840160208301375f602083830101528093505050509250925092565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6103db602083018461039b565b9392505050565b5f602082840312156103f2575f5ffd5b81356103db8161029c565b6001600160a01b0383168152604060208201525f61041e604083018461039b565b94935050505056fea2646970667358221220ffc9d973805dfc91e6aa8057ef447834a4886f84e888667d7876cc4b70ec718c64736f6c637828302e382e33312d646576656c6f702e323032362e342e32392b636f6d6d69742e6364393136336438005900000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + } + ], + "isFixedGasLimit": false, + "hasShieldedArgs": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x43f8f6", + "logs": [ + { + "address": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "blockHash": "0x7cf0b262a7e5f6037dd69fccf7c17e5c3a6023447b8f1073536ffe2f8fa1eb6c", + "blockNumber": "0xe75a76", + "blockTimestamp": "0x19e97e6e27b", + "transactionHash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000020000000000000000000000000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x6b2a685a27be27965f1c1f370693388cb6d1a57a314738804b0f642bf0150c5f", + "transactionIndex": "0x0", + "blockHash": "0x7cf0b262a7e5f6037dd69fccf7c17e5c3a6023447b8f1073536ffe2f8fa1eb6c", + "blockNumber": "0xe75a76", + "gasUsed": "0x43f8f6", + "effectiveGasPrice": "0x8", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": null, + "contractAddress": "0x268b6e7e1ef3f3eab7aab5b20286ab51997223d9" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0xfcabf", + "logs": [ + { + "address": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "topics": [ + "0x2feea65dd4e9f9cbd86b74b7734210c59a1b2981b5b137bd0ee3e208200c9067", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85", + "0xb14885de9d6f68558ba28817a491949048e11bff2c274a28b8042ca157168233" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000268b6e7e1ef3f3eab7aab5b20286ab51997223d9" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x109b88c1c8d528799ca6f455418979dd2a552493f14553ce44443b23f7df8b35", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x2", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x3", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x77f12a3c9f87d4602fe59bb8d2b68c7b516e0cacba414a53e74ea75d435dc18d", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x4", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x5", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0x4a5e9eb1ba56d04185ff75ebf0f4f42a3d7c88c35b4a90fa278437e0c9bdceca", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x6", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "0xc66b3536568140ce119bcc21a4fa7e3449a56fb5f260d32ff8e719230264132c", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "0x0000000000000000000000004406d3ce5a81f566df5d9120e5abce3a27b19e85" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x7", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x8", + "removed": false + }, + { + "address": "0x3471d21118f19bfdb84591a92c82546c74f2f321", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000012b1a4226ba7d9ad492779c924b0fc00bdcb6217" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0x9", + "removed": false + }, + { + "address": "0xb3b2f21f9a6a5d698d9178986fa4148260b5d018", + "topics": [ + "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003471d21118f19bfdb84591a92c82546c74f2f321", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0xa", + "removed": false + }, + { + "address": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "topics": [ + "0x4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b511", + "0x000000000000000000000000b3b2f21f9a6a5d698d9178986fa4148260b5d018" + ], + "data": "0x", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "blockTimestamp": "0x19e97e6ea8c", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "logIndex": "0xb", + "removed": false + } + ], + "logsBloom": "0x0002000408000000000000200000020040000000000000100880000020000000000000000000000000000000000000000040000000020402000000002000000000000000000000000000008002000200000100000000a000000410000000400000000000228080000000000000000800000000800020000000000000000000400000000000000000000800000000008000010000400080400000004000900000000000000100000020000000000000000000000000000000001000004000000000000020000000000200001000000000002000002404080100002000000020000000800000000200020000000000000800000400000000001000000000000000", + "type": "0x2", + "transactionHash": "0x248bb6469df2f154784da5ac647b62e806e94d65793e1e42081f5de60cc39eee", + "transactionIndex": "0x0", + "blockHash": "0xa959b041676b6b45e8ada260a8952c88c29b83d96739e2e8d6971683f5f47b1f", + "blockNumber": "0xe75a7e", + "gasUsed": "0xfcabf", + "effectiveGasPrice": "0x8", + "from": "0x12b1a4226ba7d9ad492779c924b0fc00bdcb6217", + "to": "0xba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed", + "contractAddress": null + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1780664953484, + "chain": 5124, + "commit": "2cb7a6c" +} diff --git a/build.sh b/build.sh index 0aa13858..c8c220f0 100755 --- a/build.sh +++ b/build.sh @@ -11,10 +11,20 @@ while getopts p:s flag; do done export FOUNDRY_PROFILE=$profile + +# Use the Seismic-flavored forge for the seismic profile (mercury EVM, ssolc). +# Stock forge can't parse shielded types like suint256. +if [ "$FOUNDRY_PROFILE" = "seismic" ]; then + forge_bin=sforge +else + forge_bin=forge +fi + echo Using profile: $FOUNDRY_PROFILE +echo Forge binary: $forge_bin if [ "$sizes" = false ]; then - forge build --skip '*/test/**' '*/script/**' --extra-output-files abi + "$forge_bin" build --skip '*/test/**' '*/script/**' --extra-output-files abi else - forge build --skip '*/test/**' '*/script/**' --extra-output-files abi --sizes + "$forge_bin" build --skip '*/test/**' '*/script/**' --extra-output-files abi --sizes fi diff --git a/deployments/5124.json b/deployments/5124.json new file mode 100644 index 00000000..ad0bfdc9 --- /dev/null +++ b/deployments/5124.json @@ -0,0 +1,6 @@ +{ + "extensionAddresses": ["0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018"], + "extensionNames": ["Seismic Dollar"], + "swapAdapter": "0x0000000000000000000000000000000000000000", + "swapFacility": "0xB6807116b3B1B321a390594e31ECD6e0076f6278" +} diff --git a/foundry.lock b/foundry.lock index 6f4bf9a1..b0b77c54 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,8 +1,8 @@ { "lib/common": { - "tag": { - "name": "v1.5.0", - "rev": "613d2d95a476612270324ecbe92317eb5e9bc98f" + "branch": { + "name": "feat/erc20-virtual", + "rev": "a1fbf37b0ab10b0f8e71223793a0fd6af77b527d" } }, "lib/forge-std": { diff --git a/foundry.toml b/foundry.toml index 0b0952a2..46069e0f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,3 +1,6 @@ +# [profile.seismic] inherits optimizer/ffi/build_info/remappings from this profile — +# load-bearing for the deployed Seismic bytecode. ssolc ignores `solc_version`. +# evm_version must stay "cancun" here so stock-forge `clean`/`update` keep working. [profile.default] evm_version = "cancun" gas_reports = ["*"] @@ -27,18 +30,12 @@ lint_on_build = false build_info = true sizes = true -# Seismic profile — builds MYieldToOne with ssolc (0.8.31). Activate: `FOUNDRY_PROFILE=seismic sforge build`. -# `evm_version = "mercury"` is the Seismic EVM revision that supports shielded types (suint/sbool/saddress/sbytes); -# the default profile stays cancun + solc 0.8.26 so sibling-extension builds remain audit-reproducible. -# `optimizer_runs = 800` (vs default 19 999): the shielded closure pushes JMIExtension to 25 853 B at 19 999, -# over the EIP-170 24 576 B limit; 800 is the size/gas knee → 23 240 B (+1 336 headroom). -# `no_match_path` skips, under this profile only: (1) test/integration/** — forks ETH mainnet, whose RPC lacks -# `eth_getFlaggedStorageAt` for shielded reads (run on Seismic devnet instead); (2) JMIExtension.t.sol — written -# against the unshielded `balanceOf`, so it trips `Unauthorized()`; JMI runs under the default profile. +# Seismic profile (ssolc / mercury) — the only profile that compiles this branch. Activate: `FOUNDRY_PROFILE=seismic sforge build`. [profile.seismic] -evm_version = "mercury" -optimizer_runs = 800 -no_match_path = "test/{integration/**,unit/projects/JMIExtension.t.sol}" +evm_version = "mercury" # Seismic EVM revision with shielded types (suint/sbool/saddress/sbytes) +optimizer_runs = 800 # default 19_999 puts JMIExtension at 25_853 B, over EIP-170; 800 → 23_240 B +force = true # full rebuilds: OZ upgrades-core validation flakes on partial build-info +no_match_path = "test/integration/**" # mainnet-fork RPCs lack eth_getFlaggedStorageAt; run via `make integration` on Seismic devnet [fuzz] runs = 5_000 @@ -48,13 +45,6 @@ runs = 512 # The number of calls to make in the invariant tests depth = 25 # The number of times to run the invariant tests fail_on_revert = true # Fail the test if the contract reverts -[profile.ci.fuzz] -runs = 10_000 - -[profile.ci.invariant] -runs = 512 -depth = 250 - [rpc_endpoints] localhost = "${LOCALHOST_RPC_URL}" mainnet = "${MAINNET_RPC_URL}" diff --git a/package.json b/package.json index d0c4b635..efa63a33 100644 --- a/package.json +++ b/package.json @@ -15,23 +15,18 @@ "scripts": { "build": "make -B build", "clean": "make -B clean", - "compile": "forge compile", "coverage": "make -B coverage", - "deploy-local": "make -B deploy-local", - "deploy-sepolia": "make -B deploy-sepolia", - "doc": "forge doc --serve --port 4000", "lint-staged": "lint-staged", "prepack": "npm run clean && npm run build", "prepare": "husky", "prettier": "prettier --write 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", - "slither": "forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --compile-force-framework foundry --ignore-compile --config-file slither.config.json --fail-high .", + "slither": "make -B slither", "solhint": "solhint -f stylish 'src/**/*.sol'", "solhint-fix": "solhint --fix 'src/**/*.sol'", "test": "make -B tests", "test-gas": "make -B gas-report", "test-fuzz": "make -B fuzz", - "test-integration": "make -B integration", - "test-invariant": "make -B invariant" + "test-integration": "make -B integration" }, "devDependencies": { "husky": "9.1.7", diff --git a/script/ConfigureSeismicExtension.s.sol b/script/ConfigureSeismicExtension.s.sol new file mode 100644 index 00000000..5f261c7a --- /dev/null +++ b/script/ConfigureSeismicExtension.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +import { console } from "../lib/forge-std/src/console.sol"; + +import { ScriptBase } from "./ScriptBase.s.sol"; + +import { IMYieldToOne } from "../src/projects/yieldToOne/interfaces/IMYieldToOne.sol"; +import { ISwapFacility } from "../src/swap/interfaces/ISwapFacility.sol"; + +/** + * @title ConfigureSeismicExtension + * @notice Post-deploy configuration for a Seismic extension: approves the extension on the + * SwapFacility and allowlists the infra contracts (Portal, LimitOrderProtocol). + * @dev Plain txs only — the contract encryption key is installed separately via + * script/set-contract-key.sh (see its header for why that step cannot live here). + */ +contract ConfigureSeismicExtension is ScriptBase { + function run() public { + address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); + address extension = vm.envAddress("EXTENSION_PROXY"); + address swapFacility = _getSwapFacility(); + + // LIMIT_ORDER_PROTOCOL is optional: not yet deployed on Seismic testnet. + address limitOrderProtocol = vm.envOr("LIMIT_ORDER_PROTOCOL", address(0)); + + address[] memory infra = new address[](limitOrderProtocol == address(0) ? 1 : 2); + infra[0] = vm.envAddress("PORTAL"); + if (limitOrderProtocol != address(0)) infra[1] = limitOrderProtocol; + + vm.startBroadcast(deployer); + + IMYieldToOne(extension).setAllowlisted(infra, true); + + vm.stopBroadcast(); + + console.log("Extension approved on SwapFacility:", extension); + console.log("Allowlisted Portal:", infra[0]); + if (limitOrderProtocol != address(0)) console.log("Allowlisted LimitOrderProtocol:", limitOrderProtocol); + } +} diff --git a/script/decrypt-transfer-event.py b/script/decrypt-transfer-event.py new file mode 100755 index 00000000..84b5ac1c --- /dev/null +++ b/script/decrypt-transfer-event.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +"""Decrypt an encrypted Transfer(bytes)/Approval(bytes) payload emitted by MYieldToOne. + +Reproduces the contract's emit pipeline off-chain (pure stdlib, no dependencies): + 1. ECDH secp256k1(privkey, peer-pubkey) -> sha256(compressed shared point) [libsecp256k1 convention] + 2. KDF #1 HKDF-SHA256(salt=none, info="aes-gcm key") [inside precompile 0x65] + 3. KDF #2 HKDF-SHA256(salt=none, info="seismic_hkdf_105") [precompile 0x68] + 4. nonce first 12 bytes of keccak256(abi.encode(from, to, nonceCounter)) + 5. AES-256-GCM decrypt (ciphertext || 16-byte tag) [inverse of precompile 0x66] +ECDH is symmetric: pass either (recipient privkey + contract pubkey) or (contract privkey ++ recipient pubkey). The plaintext is abi.encode(uint256 amount). + +USAGE + decrypt-transfer-event.py --privkey --peer-pubkey + --from
--to
--ciphertext + (--nonce-counter | --scan ) + decrypt-transfer-event.py --self-test + + --privkey decryption private key (recipient's, or the contract's) + --peer-pubkey the OTHER party's 33-byte compressed public key + --from event `from` topic (sender / approver) — bound into the nonce + --to event `to` topic (recipient / spender) — bound into the nonce + --nonce-counter the contract's encryptedEventNonce value used for this emit (1-based) + --scan try counters 1..max-n until the GCM tag verifies (when the counter is unknown) + --ciphertext event payload hex (ciphertext || 16-byte tag) + --self-test verify against vectors pinned from the Seismic precompiles (sforge mercury EVM) + +Uses the 'cryptography' package for AES-GCM when installed; otherwise falls back to a +built-in pure-Python AES-GCM (validated against the same vectors). +""" + +import argparse +import hashlib +import hmac +import sys + +# ============ secp256k1 ============ + +P = 2**256 - 2**32 - 977 +N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 +GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + + +def _point_add(a, b): + if a is None: + return b + if b is None: + return a + (x1, y1), (x2, y2) = a, b + if x1 == x2 and (y1 + y2) % P == 0: + return None + if a == b: + lam = (3 * x1 * x1) * pow(2 * y1, P - 2, P) % P + else: + lam = (y2 - y1) * pow(x2 - x1, P - 2, P) % P + x3 = (lam * lam - x1 - x2) % P + return (x3, (lam * (x1 - x3) - y1) % P) + + +def _scalar_mult(k, point): + result = None + while k: + if k & 1: + result = _point_add(result, point) + point = _point_add(point, point) + k >>= 1 + return result + + +def decompress_pubkey(pub: bytes): + if len(pub) != 33 or pub[0] not in (2, 3): + raise ValueError("expected a 33-byte compressed public key") + x = int.from_bytes(pub[1:], "big") + y_sq = (pow(x, 3, P) + 7) % P + y = pow(y_sq, (P + 1) // 4, P) + if y * y % P != y_sq: + raise ValueError("public key x-coordinate is not on secp256k1") + if y % 2 != pub[0] % 2: + y = P - y + return (x, y) + + +def ecdh_shared_secret(privkey: bytes, peer_pubkey: bytes) -> bytes: + """libsecp256k1 ECDH: sha256 of the compressed shared point.""" + k = int.from_bytes(privkey, "big") + if not 0 < k < N: + raise ValueError("private key out of range") + x, y = _scalar_mult(k, decompress_pubkey(peer_pubkey)) + return hashlib.sha256(bytes([2 if y % 2 == 0 else 3]) + x.to_bytes(32, "big")).digest() + + +# ============ HKDF-SHA256 (RFC 5869) ============ + + +def hkdf_sha256(ikm: bytes, info: bytes, length: int = 32) -> bytes: + prk = hmac.new(b"\x00" * 32, ikm, hashlib.sha256).digest() + okm, t, i = b"", b"", 1 + while len(okm) < length: + t = hmac.new(prk, t + info + bytes([i]), hashlib.sha256).digest() + okm += t + i += 1 + return okm[:length] + + +def derive_aes_key(privkey: bytes, peer_pubkey: bytes) -> bytes: + """Full key pipeline: ECDH -> HKDF("aes-gcm key") [0x65] -> HKDF("seismic_hkdf_105") [0x68].""" + return hkdf_sha256(hkdf_sha256(ecdh_shared_secret(privkey, peer_pubkey), b"aes-gcm key"), b"seismic_hkdf_105") + + +# ============ keccak-256 (for the event nonce) ============ + +_KECCAK_RC = [ + 0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000, + 0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, + 0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A, + 0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003, + 0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A, + 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008, +] +_KECCAK_ROT = [ + [0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61], [28, 55, 25, 21, 56], [27, 20, 39, 8, 14], +] +_M64 = (1 << 64) - 1 + + +def _rol(v, s): + return ((v << s) | (v >> (64 - s))) & _M64 + + +def _keccak_f(state): + for rc in _KECCAK_RC: + c = [state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4] for x in range(5)] + d = [c[(x - 1) % 5] ^ _rol(c[(x + 1) % 5], 1) for x in range(5)] + state = [[state[x][y] ^ d[x] for y in range(5)] for x in range(5)] + b = [[0] * 5 for _ in range(5)] + for x in range(5): + for y in range(5): + b[y][(2 * x + 3 * y) % 5] = _rol(state[x][y], _KECCAK_ROT[x][y]) + state = [[b[x][y] ^ ((~b[(x + 1) % 5][y]) & b[(x + 2) % 5][y]) for y in range(5)] for x in range(5)] + state[0][0] ^= rc + return state + + +def keccak256(data: bytes) -> bytes: + rate = 136 + data = data + b"\x01" + b"\x00" * (rate - len(data) % rate - 1) + data = data[:-1] + bytes([data[-1] | 0x80]) + state = [[0] * 5 for _ in range(5)] + for block in range(0, len(data), rate): + for i in range(rate // 8): + state[i % 5][i // 5] ^= int.from_bytes(data[block + 8 * i : block + 8 * i + 8], "little") + state = _keccak_f(state) + out = b"" + for i in range(4): + out += state[i % 5][i // 5].to_bytes(8, "little") + return out + + +def event_nonce(from_addr: str, to_addr: str, counter: int) -> bytes: + """First 12 bytes of keccak256(abi.encode(from, to, counter)).""" + encoded = ( + int(from_addr, 16).to_bytes(32, "big") + int(to_addr, 16).to_bytes(32, "big") + counter.to_bytes(32, "big") + ) + return keccak256(encoded)[:12] + + +# ============ AES-256-GCM ============ + +_AES_SBOX = None + + +def _aes_init(): + global _AES_SBOX + if _AES_SBOX is not None: + return + sbox = [0] * 256 + p = q = 1 + while True: + p = p ^ ((p << 1) & 0xFF) ^ (0x1B if p & 0x80 else 0) + q ^= q << 1 + q ^= q << 2 + q ^= q << 4 + q &= 0xFF + q ^= 0x09 if q & 0x80 else 0 + sbox[p] = (q ^ _rol8(q, 1) ^ _rol8(q, 2) ^ _rol8(q, 3) ^ _rol8(q, 4)) ^ 0x63 + if p == 1: + break + sbox[0] = 0x63 + _AES_SBOX = sbox + + +def _rol8(v, s): + return ((v << s) | (v >> (8 - s))) & 0xFF + + +def _xtime(a): + return ((a << 1) ^ 0x1B) & 0xFF if a & 0x80 else a << 1 + + +def _aes256_expand_key(key): + _aes_init() + words = [list(key[4 * i : 4 * i + 4]) for i in range(8)] + rcon = 1 + for i in range(8, 60): + t = list(words[i - 1]) + if i % 8 == 0: + t = [_AES_SBOX[b] for b in t[1:] + t[:1]] + t[0] ^= rcon + rcon = _xtime(rcon) + elif i % 8 == 4: + t = [_AES_SBOX[b] for b in t] + words.append([words[i - 8][j] ^ t[j] for j in range(4)]) + return [bytes(sum((words[4 * r + c] for c in range(4)), [])) for r in range(15)] + + +def _aes256_encrypt_block(round_keys, block): + state = [block[i] ^ round_keys[0][i] for i in range(16)] + for rnd in range(1, 15): + state = [_AES_SBOX[b] for b in state] + state = [state[(i + 4 * (i % 4)) % 16] for i in range(16)] # shift rows (column-major layout) + if rnd < 14: + mixed = [] + for c in range(4): + col = state[4 * c : 4 * c + 4] + mixed += [ + _xtime(col[0]) ^ _xtime(col[1]) ^ col[1] ^ col[2] ^ col[3], + col[0] ^ _xtime(col[1]) ^ _xtime(col[2]) ^ col[2] ^ col[3], + col[0] ^ col[1] ^ _xtime(col[2]) ^ _xtime(col[3]) ^ col[3], + _xtime(col[0]) ^ col[0] ^ col[1] ^ col[2] ^ _xtime(col[3]), + ] + state = mixed + state = [state[i] ^ round_keys[rnd][i] for i in range(16)] + return bytes(state) + + +def _ghash(h_int, data): + y = 0 + for i in range(0, len(data), 16): + y ^= int.from_bytes(data[i : i + 16], "big") + z, v = 0, y + for bit in range(128): + if (h_int >> (127 - bit)) & 1: + z ^= v + v = (v >> 1) ^ (0xE1 << 120) if v & 1 else v >> 1 + y = z + return y + + +def aes256_gcm_decrypt(key: bytes, nonce: bytes, payload: bytes) -> bytes: + """Decrypt ciphertext||tag; raises ValueError on tag mismatch. Pure-Python fallback path.""" + try: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM # type: ignore + + return AESGCM(key).decrypt(nonce, payload, None) + except ImportError: + pass + + if len(payload) < 16: + raise ValueError("payload shorter than a GCM tag") + ciphertext, tag = payload[:-16], payload[-16:] + + round_keys = _aes256_expand_key(key) + h_int = int.from_bytes(_aes256_encrypt_block(round_keys, b"\x00" * 16), "big") + j0 = nonce + b"\x00\x00\x00\x01" + + padded = ciphertext + b"\x00" * (-len(ciphertext) % 16) + lengths = (0).to_bytes(8, "big") + (8 * len(ciphertext)).to_bytes(8, "big") + s = _ghash(h_int, padded + lengths) + expected_tag = (s ^ int.from_bytes(_aes256_encrypt_block(round_keys, j0), "big")).to_bytes(16, "big") + if not hmac.compare_digest(expected_tag, tag): + raise ValueError("GCM tag mismatch (wrong key, nonce, or corrupted ciphertext)") + + plaintext = b"" + counter = int.from_bytes(j0, "big") + for i in range(0, len(ciphertext), 16): + counter += 1 + keystream = _aes256_encrypt_block(round_keys, counter.to_bytes(16, "big")) + block = ciphertext[i : i + 16] + plaintext += bytes(a ^ b for a, b in zip(block, keystream)) + return plaintext + + +# ============ decryption entry points ============ + + +def decrypt(privkey, peer_pubkey, from_addr, to_addr, counter, payload) -> int: + key = derive_aes_key(privkey, peer_pubkey) + plaintext = aes256_gcm_decrypt(key, event_nonce(from_addr, to_addr, counter), payload) + if len(plaintext) != 32: + raise ValueError(f"expected abi.encode(uint256) plaintext, got {len(plaintext)} bytes") + return int.from_bytes(plaintext, "big") + + +def scan_decrypt(privkey, peer_pubkey, from_addr, to_addr, max_counter, payload): + for counter in range(1, max_counter + 1): + try: + return counter, decrypt(privkey, peer_pubkey, from_addr, to_addr, counter, payload) + except ValueError: + continue + raise ValueError(f"no counter in 1..{max_counter} produced a valid GCM tag") + + +# ============ self-test (vectors pinned from the Seismic precompiles) ============ + + +def self_test(): + # 0x65 with privkey=1, peer=G (test/integration/seismic/MYieldToOneSeismic.t.sol) + out = hkdf_sha256(ecdh_shared_secret((1).to_bytes(32, "big"), bytes.fromhex( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")), b"aes-gcm key") + assert out.hex() == "a59676edf7d8f47a0cc8ac42e29566d4a1763eeeba8794d6a196029a1477f147", "ECDH(0x65) vector" + + # 0x68 with ikm = uint256(0xdeadbeef) + out = hkdf_sha256((0xDEADBEEF).to_bytes(32, "big"), b"seismic_hkdf_105") + assert out.hex() == "eb3cb17dcdc55d1119c98cac1e12bb13a95f81b50a0784750bdcf92787c4985e", "HKDF(0x68) vector" + + # 0x66/0x67 with key=uint256(0x42), nonce=uint96(7), plaintext=abi.encode(123456) + payload = bytes.fromhex( + "bae51be0c992f7c0c34d03a9431aebdc7732c399729054925a094d8f373d65c18c9eec49034da029df615639016e16c8") + plaintext = aes256_gcm_decrypt((0x42).to_bytes(32, "big"), (7).to_bytes(12, "big"), payload) + assert plaintext == (123456).to_bytes(32, "big"), "AES-GCM(0x66) vector" + + # keccak256 sanity (empty-input digest) + assert keccak256(b"").hex() == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", "keccak256" + + # Full pipeline vector emitted by test_shieldedTransfer_realKeys_recipientDecrypts (sforge, real precompiles) + amount = decrypt( + bytes.fromhex("af22e8d07271a11eb94550f14173e09ce37902708dfc142a4bc1884ffcb7e688"), + bytes.fromhex("03a0a51f0cf98915ae76cfcf9d3993a0e35bbbed7a457dbcf66ed6500126e2be1f"), + "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", + "0x61469988af724818Ab0078FC6BFd2c8ddCf174A9", + 1, + bytes.fromhex("9acb56d05afbec4d447e084de68773a265f475247ccd7e4f1a2371e3711ca7fca582491634286d9be37f457c0b6d6877"), + ) + assert amount == 1_000_000_000, f"full pipeline vector: got {amount}" + + # --scan finds the same counter + counter, amount = scan_decrypt( + bytes.fromhex("af22e8d07271a11eb94550f14173e09ce37902708dfc142a4bc1884ffcb7e688"), + bytes.fromhex("03a0a51f0cf98915ae76cfcf9d3993a0e35bbbed7a457dbcf66ed6500126e2be1f"), + "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", + "0x61469988af724818Ab0078FC6BFd2c8ddCf174A9", + 16, + bytes.fromhex("9acb56d05afbec4d447e084de68773a265f475247ccd7e4f1a2371e3711ca7fca582491634286d9be37f457c0b6d6877"), + ) + assert (counter, amount) == (1, 1_000_000_000), "scan vector" + + print("self-test: all vectors pass") + + +# ============ CLI ============ + + +def _hex_bytes(value): + return bytes.fromhex(value[2:] if value.startswith("0x") else value) + + +def main(): + if len(sys.argv) == 1: + print(__doc__) + sys.exit(2) + + parser = argparse.ArgumentParser(add_help=True, description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--privkey", type=_hex_bytes) + parser.add_argument("--peer-pubkey", type=_hex_bytes) + parser.add_argument("--from", dest="from_addr") + parser.add_argument("--to", dest="to_addr") + parser.add_argument("--nonce-counter", type=int) + parser.add_argument("--scan", type=int, metavar="MAX_N") + parser.add_argument("--ciphertext", type=_hex_bytes) + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + + if args.self_test: + self_test() + return + + required = [args.privkey, args.peer_pubkey, args.from_addr, args.to_addr, args.ciphertext] + if any(v is None for v in required) or (args.nonce_counter is None and args.scan is None): + parser.error("need --privkey, --peer-pubkey, --from, --to, --ciphertext and one of --nonce-counter/--scan") + + if args.nonce_counter is not None: + counter = args.nonce_counter + amount = decrypt(args.privkey, args.peer_pubkey, args.from_addr, args.to_addr, counter, args.ciphertext) + else: + counter, amount = scan_decrypt( + args.privkey, args.peer_pubkey, args.from_addr, args.to_addr, args.scan, args.ciphertext + ) + + print(f"nonce-counter: {counter}") + print(f"amount: {amount}") + + +if __name__ == "__main__": + main() diff --git a/script/set-contract-key.sh b/script/set-contract-key.sh new file mode 100755 index 00000000..12a06d25 --- /dev/null +++ b/script/set-contract-key.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# One-shot install of the extension's encryption keypair via `scast send --seismic`. +# NEVER port this to a .s.sol: sforge script has no --seismic broadcast, so a plain tx +# would publish the contract private key in plaintext calldata forever (setContractKey +# is one-shot; rotation is impossible). Generate a FRESH keypair per derived deployment; +# archive both keys in 1Password — the off-chain copy only grants ops decryption +# (recipients decrypt with their own keys via ECDH symmetry, never the contract key). +# Env: EXTENSION_PROXY, RPC_URL, PRIVATE_KEY (admin). NONINTERACTIVE=1 skips the pause. +set -euo pipefail + +: "${EXTENSION_PROXY:?EXTENSION_PROXY is required}" +: "${RPC_URL:?RPC_URL is required}" +: "${PRIVATE_KEY:?PRIVATE_KEY is required}" + +SCAST="$(dirname "$0")/../.seismic-toolchain/bin/scast" +if [ ! -x "$SCAST" ]; then + echo "error: scast not found at $SCAST — run: source scripts/seismic-env.sh && sfoundryup" >&2 + exit 1 +fi + +# Preflight: refuse to overwrite an installed key (the guard on-chain is one-shot too, +# but failing here avoids burning the freshly generated keypair). +existing="$(cast call "$EXTENSION_PROXY" "contractPublicKey()(bytes)" --rpc-url "$RPC_URL")" +if [ "$existing" != "0x" ]; then + echo "error: contractPublicKey() is already set on $EXTENSION_PROXY: $existing" >&2 + exit 1 +fi + +# Keygen: fresh secp256k1 keypair; compress the 64-byte pubkey to 33 bytes +# (0x02/0x03 prefix by y parity), which is the format setContractKey stores. +contract_privkey="$(cast wallet new | awk '/Private key:/ { print $3 }')" +uncompressed="$(cast wallet public-key --raw-private-key "$contract_privkey")" +hex="${uncompressed#0x}" +if [ "${#hex}" -ne 128 ]; then + echo "error: unexpected public key length from cast wallet public-key: $uncompressed" >&2 + exit 1 +fi +x="${hex:0:64}" +y="${hex:64:64}" +case "${y: -1}" in + 0 | 2 | 4 | 6 | 8 | a | c | e | A | C | E) prefix=02 ;; + *) prefix=03 ;; +esac +contract_pubkey="0x${prefix}${x}" + +echo "Contract private key: $contract_privkey" +echo "Contract public key (compressed): $contract_pubkey" +if [ "${NONINTERACTIVE:-0}" != "1" ]; then + read -rp "Archive BOTH keys in 1Password now, then press enter to continue..." _ +fi + +"$SCAST" send --seismic "$EXTENSION_PROXY" "setContractKey(sbytes32,bytes)" \ + "$contract_privkey" "$contract_pubkey" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" + +# Postflight: assert the installed key matches what we generated. +after="$(cast call "$EXTENSION_PROXY" "contractPublicKey()(bytes)" --rpc-url "$RPC_URL" | tr '[:upper:]' '[:lower:]')" +expected="$(echo "$contract_pubkey" | tr '[:upper:]' '[:lower:]')" +if [ "$after" != "$expected" ]; then + echo "error: postflight mismatch — contractPublicKey() = $after, expected $expected" >&2 + exit 1 +fi + +echo "Contract key installed on $EXTENSION_PROXY" diff --git a/script/verify-seismic.py b/script/verify-seismic.py index bb36ef95..c866d342 100644 --- a/script/verify-seismic.py +++ b/script/verify-seismic.py @@ -2,34 +2,11 @@ """ Auto-verify every contract from a Seismic (mercury/ssolc) deploy broadcast. -WHY THIS EXISTS - Inline `sforge script --verify` can't verify mercury builds: the socialscan - explorer rejects `evmVersion: mercury` in the standard-json and wants the - compiler labelled `v0.8.31+commit.cd9163d8` (no `-develop.` tag), and - forge offers no flag to strip evmVersion on the script-verify path. This - script does what forge can't: it reconstructs the EXACT ssolc standard-json - from the local build artifacts, drops evmVersion, fixes the compiler label, - and submits each deployed contract to the explorer's etherscan-style API. - -WHAT IT DOES - 1. Reads the newest broadcast run for a deploy script + chain id. - 2. Discovers every deployed contract (direct CREATEs + factory/CreateX - additionalContracts) and identifies each by matching its on-chain - creation code against the compiled bytecode in out/build-info/*.json - (no name list to maintain — it's all bytecode-derived). - 3. For each match, builds the minimal standard-json (import closure only), - removes settings.evmVersion, derives constructor args from the init code, - and POSTs verifysourcecode. Contracts whose bytecode isn't ours (e.g. the - CreateX internal proxy) are skipped and logged. - - Optimizer runs / viaIR are read from the build-info, NOT hardcoded, so this - works across repos with different seismic profiles (e.g. runs=200 + via-ir). - -REQUIREMENTS - - `out/build-info/*.json` must be the FULL build-info (input + output). Enable - with `build_info = true` in foundry.toml, and run this right after the - seismic deploy so out/ matches what was deployed. - - python3 + jq. No ssolc/sforge needed. +Forge can't verify mercury builds (the explorer rejects `evmVersion: mercury` and wants the +compiler labelled `v0.8.31+commit.cd9163d8`), so this rebuilds the exact ssolc standard-json +from out/build-info (closure only, evmVersion dropped), matches each deployed address by +creation bytecode, derives constructor args from the init code, and POSTs verifysourcecode. +Needs python3 + jq and FULL build-info (`build_info = true`), fresh from the deploy build. USAGE python3 script/verify-seismic.py [--dry-run] diff --git a/scripts/seismic-env.sh b/scripts/seismic-env.sh index 4790e74a..708c0c1a 100755 --- a/scripts/seismic-env.sh +++ b/scripts/seismic-env.sh @@ -1,35 +1,14 @@ #!/usr/bin/env bash -# Seismic toolchain environment — sources sforge / ssolc / sanvil from a -# repo-local install at .seismic-toolchain/ (git-ignored). +# Seismic toolchain env — puts the repo-local sforge/ssolc/sanvil install at .seismic-toolchain/ (git-ignored) on PATH. # -# USAGE -# From the repo root in any bash/zsh shell: -# source scripts/seismic-env.sh -# After sourcing, `sforge`, `ssolc`, and `sanvil` resolve to the repo-local -# binaries. The export survives until the shell exits. +# Usage: source scripts/seismic-env.sh +# (any bash/zsh shell; lasts until the shell exits; sforge auto-uses FOUNDRY_PROFILE=seismic) # -# FIRST-TIME INSTALL -# 1. source scripts/seismic-env.sh -# 2. curl -L -H "Accept: application/vnd.github.v3.raw" \ -# "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash -# The installer writes sfoundryup to $FOUNDRY_DIR/bin/sfoundryup. -# 3. sfoundryup -# Fetches sforge / ssolc / sanvil into $FOUNDRY_DIR/bin. -# 4. sforge --version && ssolc --version && sanvil --version -# -# NOTES -# - FOUNDRY_DIR is the env var the Seismic installer honors. Pointing it at -# the repo-local path overrides the default of "$HOME/.seismic". -# - Pre-prepending $FOUNDRY_DIR/bin to PATH causes the installer's rc-edit -# check to find the bin dir already on PATH and skip mutating ~/.zshenv -# on most versions of the upstream script. If a future installer revision -# still appends an export line, remove it from ~/.zshenv by hand — the -# binaries continue to work via this sourced env. -# - Coexists with stock Foundry: `forge` (stock) and `sforge` (Seismic) live -# in different bin dirs and have different names — no collisions. +# Install: 1. source scripts/seismic-env.sh +# 2. curl -L -H "Accept: application/vnd.github.v3.raw" "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash +# 3. sfoundryup (fetches sforge/ssolc/sanvil into $FOUNDRY_DIR/bin; check with sforge --version) -# Resolve repo root from this script's own path (works whether sourced from -# repo root, from a subdir, or from $PATH after `chmod +x`). +# Resolve repo root from this script's own path (works when sourced from any cwd). __SEISMIC_ENV_SH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" SEISMIC_REPO_ROOT="$(cd "$__SEISMIC_ENV_SH_DIR/.." && pwd)" @@ -42,11 +21,7 @@ case ":$PATH:" in *) export PATH="$FOUNDRY_BIN_DIR:$PATH" ;; esac -# Shell function that pins FOUNDRY_PROFILE=seismic for sforge invocations so -# `evm_version = "mercury"` (required for shielded types) is applied without -# the caller having to remember the flag. `forge` (stock) is untouched and -# continues using the default profile + cancun. -# Override per-call with `FOUNDRY_PROFILE=default sforge ...` if needed. +# Pin FOUNDRY_PROFILE=seismic for sforge (mercury EVM, shielded types); stock `forge` is untouched; override per-call with `FOUNDRY_PROFILE=default sforge ...`. sforge() { FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-seismic}" command sforge "$@" } diff --git a/src/projects/yieldToOne/MYieldToOne.sol b/src/projects/yieldToOne/MYieldToOne.sol index 12777214..88e2f92f 100644 --- a/src/projects/yieldToOne/MYieldToOne.sol +++ b/src/projects/yieldToOne/MYieldToOne.sol @@ -18,19 +18,14 @@ abstract contract MYieldToOneStorageLayout { uint256 totalSupply; address yieldRecipient; mapping(address account => suint256 balance) balanceOf; - // Sole allowance store, written by BOTH the shielded and native (ABI-cast) approve/transferFrom. - // The inherited ERC20Extended `allowance` slot is never written (native writes here; permit reverts). + // Sole allowance store; the inherited ERC20Extended `allowance` slot is never written. mapping(address account => mapping(address spender => suint256 allowance)) shieldedAllowance; - // Admin-curated trusted M0 infra; gates the native approve/transferFrom paths and balanceOf reads. mapping(address account => bool isAllowlisted) allowlist; - // Encrypted Transfer events: per-recipient public-key registry. An unset key triggers the - // empty-ciphertext fallback emit; the recipient still recovers the amount via its gated balanceOf. mapping(address account => bytes publicKey) publicKeys; - // Contract public key (plain bytes); off-chain decryption clients ECDH against this. bytes _contractPublicKey; - // Contract private key (shielded ECDH input); set once via `setContractKey` (MUST be sent as TxSeismic 0x4A). + // Set once via `setContractKey` (TxSeismic 0x4A only); no getter exposes it. sbytes32 contractPrivateKey; - // Monotonic counter feeding the per-emit AES-GCM nonce; pre-incremented so nonces never repeat under one key. + // Monotonic counter feeding the per-emit AES-GCM nonce; pre-incremented so nonces never repeat. uint256 encryptedEventNonce; } @@ -149,8 +144,6 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free _setYieldRecipient(account); } - /* ============ Allowlist Management ============ */ - /// @inheritdoc IMYieldToOne function setAllowlisted(address account, bool status) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { _setAllowlisted(account, status); @@ -163,19 +156,18 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } } - /* ============ Encrypted-Event Keypair Management ============ */ - /// @inheritdoc IMYieldToOne - /// @dev One-shot guard casts the shielded key to `bytes32` for a zero-check only (no write-back). + /// @dev Deliberately not folded into `initialize()`: initializer calldata is plaintext and would leak the key. function setContractKey( sbytes32 privateKey, bytes calldata publicKey ) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { - if (publicKey.length != 33) revert InvalidPublicKeyLength(); + _revertIfInvalidPublicKey(publicKey); + + if (bytes32(privateKey) == bytes32(0)) revert ZeroPrivateKey(); MYieldToOneStorageStruct storage $ = _getMYieldToOneStorageLocation(); - // One-shot guard (control-flow compare only; see @dev). if (bytes32($.contractPrivateKey) != bytes32(0)) revert ContractKeyAlreadySet(); $.contractPrivateKey = privateKey; @@ -186,41 +178,31 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free /// @inheritdoc IMYieldToOne function registerPublicKey(bytes calldata publicKey) external virtual { - if (publicKey.length != 33) revert InvalidPublicKeyLength(); + _revertIfInvalidPublicKey(publicKey); _getMYieldToOneStorageLocation().publicKeys[msg.sender] = publicKey; emit PublicKeyRegistered(msg.sender); } - /* ============ Shielded ERC20 Entry Points ============ */ - /// @inheritdoc IMYieldToOne function transfer(address recipient, suint256 amount) external returns (bool) { - // User-to-user path: encrypted-bytes emit. _shieldedTransfer(msg.sender, recipient, amount, true); return true; } /// @inheritdoc IMYieldToOne function approve(address spender, suint256 amount) external returns (bool) { - _shieldedApprove(msg.sender, spender, amount); + _shieldedApprove(msg.sender, spender, amount, true); return true; } /// @inheritdoc IMYieldToOne function transferFrom(address sender, address recipient, suint256 amount) external returns (bool) { - // User-to-user path via allowance: encrypted-bytes emit. _spendAllowanceAndTransfer(sender, recipient, amount, true); return true; } - /* ============ Inherited IERC20 / IERC20Extended Entry Points (Allowlist-Gated) ============ */ - // Re-enabled for trusted infra only: native `approve` if the SPENDER is infra, native - // `transferFrom` if the CALLER is infra (`_isInfra`). Both write the same `shieldedAllowance` - // slot as the `suint256` overloads (ABI-cast only), so the paths can't diverge. `transfer` and - // both `permit` overloads always revert. Everyone else uses the `suint256` overloads above. - /// @inheritdoc IERC20 function transfer( address /* recipient */, @@ -237,7 +219,6 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free ) external override(ERC20ExtendedUpgradeable, IERC20) returns (bool) { if (!_isInfra(msg.sender)) revert UseShieldedTransfer(); - // Infra path: amount already public via bridge calldata, so emit the plaintext Transfer. _spendAllowanceAndTransfer(sender, recipient, suint256(amount), false); return true; } @@ -249,7 +230,7 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free ) external override(ERC20ExtendedUpgradeable, IERC20) returns (bool) { if (!_isInfra(spender)) revert UseShieldedApprove(); - _shieldedApprove(msg.sender, spender, suint256(amount)); + _shieldedApprove(msg.sender, spender, suint256(amount), false); return true; } @@ -280,10 +261,12 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free /* ============ View/Pure Functions ============ */ /// @inheritdoc IERC20 - /// @dev Shielded read, gated to `account` (TxSeismic 0x4A signed read — plain eth_call zeroes - /// msg.sender and reverts) or trusted infra (`_isInfra`). Not readable by arbitrary callers. - function balanceOf(address account) public view override returns (uint256) { - if (msg.sender != account && !_isInfra(msg.sender)) revert Unauthorized(); + /// @dev Gated read: only `account` itself (signed read), trusted infra, or FREEZE_MANAGER_ROLE holders. + function balanceOf(address account) public view virtual override returns (uint256) { + if (msg.sender != account && !_isInfra(msg.sender) && !hasRole(FREEZE_MANAGER_ROLE, msg.sender)) { + revert Unauthorized(); + } + return uint256(_getMYieldToOneStorageLocation().balanceOf[account]); } @@ -293,8 +276,7 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /// @inheritdoc IERC20 - /// @dev Shielded read; requires msg.sender == owner or spender (TxSeismic 0x4A signed read). - /// Sole allowance source — the inherited unshielded `allowance` slot is never written. + /// @dev Gated read: only `owner` or `spender` (signed read) may read the shielded allowance. function allowance( address owner, address spender @@ -305,6 +287,7 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free /// @inheritdoc IMYieldToOne function yield() public view virtual returns (uint256) { + // NOTE: Can be `unchecked` because the subtraction only runs when `balance_ > totalSupply_`. unchecked { uint256 balance_ = _mBalanceOf(address(this)); uint256 totalSupply_ = totalSupply(); @@ -426,9 +409,10 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Internal balance update used by both the inherited (now-unreachable from outside) - * and the shielded transfer paths, and by `MYieldToOneForcedTransfer._forceTransfer`. - * Casts the public `uint256` to the shielded storage type at the boundary. + * @dev Internal balance update function called on transfer. + * @param sender The sender's address. + * @param recipient The recipient's address. + * @param amount The amount to be transferred. */ function _update(address sender, address recipient, uint256 amount) internal override { MYieldToOneStorageStruct storage $ = _getMYieldToOneStorageLocation(); @@ -442,10 +426,12 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Shared allowance-spend + transfer for both `transferFrom` overloads. Decrements the - * shielded allowance (the sole store), then delegates to `_shieldedTransfer`. Reverts - * `InsufficientAllowance(spender, 0, amount)` — zeroed payload, no shielded-value leak. - * `encryptEmit` only selects the Transfer event shape (see `_shieldedTransfer`). + * @dev Decrements `msg.sender`'s shielded allowance, then transfers via `_shieldedTransfer`. + * @dev Reverts `InsufficientAllowance(msg.sender, 0, amount)` — zeroed payload, no shielded-value leak. + * @param sender The address whose tokens are being moved. + * @param recipient The address receiving the tokens. + * @param amount The shielded amount to transfer. + * @param encryptEmit Whether to emit the encrypted-bytes `Transfer` overload or the plaintext one. */ function _spendAllowanceAndTransfer(address sender, address recipient, suint256 amount, bool encryptEmit) internal { MYieldToOneStorageStruct storage $ = _getMYieldToOneStorageLocation(); @@ -453,6 +439,8 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free // Infinite-allowance shortcut (mirrors ERC20ExtendedUpgradeable.transferFrom) if (uint256(spenderAllowance) != type(uint256).max) { + // NOTE: Branching on a shielded value leaks a 1-bit comparison via revert-vs-success; + // accepted — inherent to ERC20 insufficient-allowance semantics (ssolc 10311). if (spenderAllowance < amount) revert IERC20Extended.InsufficientAllowance(msg.sender, 0, uint256(amount)); // NOTE: Can be `unchecked` because the `spenderAllowance < amount` check above guarantees @@ -466,10 +454,12 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Shielded transfer pipeline mirroring `MExtension._transfer` via a suint256→uint256 bridge; - * reuses `_beforeTransfer` (freeze/pause). Reverts `InsufficientBalance(account, 0, amount)` - * — zeroed payload. `encryptEmit`: true => encrypted-bytes Transfer (user paths); false => - * plaintext Transfer(uint256) (infra paths, amount already public). + * @dev Shielded transfer mirroring `MExtension._transfer`, including its `_beforeTransfer` hook. + * @dev Reverts `InsufficientBalance(sender, 0, amount)` — zeroed payload, no balance leak. + * @param sender The address from which the tokens are being transferred. + * @param recipient The address to which the tokens are being transferred. + * @param amount The shielded amount to transfer. + * @param encryptEmit Whether to emit the encrypted-bytes `Transfer` overload or the plaintext one. */ function _shieldedTransfer(address sender, address recipient, suint256 amount, bool encryptEmit) internal { uint256 amount_ = uint256(amount); @@ -478,13 +468,15 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free _beforeTransfer(sender, recipient, amount_); if (encryptEmit) { - _emitEncryptedTransfer(sender, recipient, amount); + emit Transfer(sender, recipient, _encryptAmount(sender, recipient, amount)); } else { emit Transfer(sender, recipient, amount_); } if (amount_ == 0) return; + // NOTE: Branching on a shielded value leaks a 1-bit comparison via revert-vs-success; + // accepted — inherent to ERC20 insufficient-balance semantics (ssolc 10311). if (_getMYieldToOneStorageLocation().balanceOf[sender] < amount) { revert InsufficientBalance(sender, 0, amount_); } @@ -492,39 +484,37 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free _update(sender, recipient, amount_); } - /* ============ Encrypted Transfer Event Pipeline ============ */ - /** - * @dev Emits the encrypted-bytes Transfer for a user-to-user shielded transfer; amount is - * AES-GCM-encrypted under HKDF(ECDH(contractPrivKey, recipientPubKey)). Unregistered - * recipient => empty-ciphertext fallback (transfer still succeeds; amount only via gated - * balanceOf). Reverts `ContractKeyNotSet` if the keypair is not installed. Nonce: see slot 8. + * @dev Encrypts `amount` to `to`'s registered key (AES-GCM under HKDF(ECDH)) for an event payload. + * @dev Reverts `ContractKeyNotSet` before the unregistered-key fallback: user emits fail uniformly pre-key. + * @param from The counterparty address bound into the nonce derivation (sender / approver). + * @param to The account whose registered key the ciphertext is encrypted to. + * @param amount The shielded amount to encrypt. + * @return The AES-GCM ciphertext, or empty bytes if `to` has not registered a key. */ - function _emitEncryptedTransfer(address from, address to, suint256 amount) internal { + function _encryptAmount(address from, address to, suint256 amount) internal returns (bytes memory) { MYieldToOneStorageStruct storage $ = _getMYieldToOneStorageLocation(); - bytes memory pubKey = $.publicKeys[to]; - - if (pubKey.length == 0) { - emit Transfer(from, to, bytes("")); - return; - } if (bytes32($.contractPrivateKey) == bytes32(0)) revert ContractKeyNotSet(); - // Pre-increment so the first nonce is 1 and no two emits reuse a nonce under one key. + bytes memory pubKey = $.publicKeys[to]; + + if (pubKey.length == 0) return bytes(""); + uint256 n = ++$.encryptedEventNonce; sbytes32 sharedSecret = _ecdh($.contractPrivateKey, pubKey); sbytes32 aesKey = _hkdf(sharedSecret); bytes12 nonce = bytes12(keccak256(abi.encode(from, to, n))); - bytes memory ciphertext = _aesGcmEncrypt(aesKey, nonce, abi.encode(uint256(amount))); - emit Transfer(from, to, ciphertext); + return _aesGcmEncrypt(aesKey, nonce, abi.encode(uint256(amount))); } /** - * @dev Seismic ECDH precompile (0x65): shared secret of the shielded `privKey` and `peerPubKey`. - * Reverts `PrecompileFailed`. + * @dev Calls the Seismic ECDH precompile (0x65); reverts `PrecompileFailed` on failure. + * @param privKey The shielded private key. + * @param peerPubKey The peer's compressed public key. + * @return The shielded ECDH shared secret. */ function _ecdh(sbytes32 privKey, bytes memory peerPubKey) internal view returns (sbytes32) { (bool success, bytes memory result) = address(0x65).staticcall(abi.encodePacked(bytes32(privKey), peerPubKey)); @@ -533,8 +523,9 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Seismic HKDF precompile (0x68): expands the shared secret into an AES-GCM key. - * Reverts `PrecompileFailed`. + * @dev Calls the Seismic HKDF precompile (0x68); reverts `PrecompileFailed` on failure. + * @param sharedSecret The shielded ECDH shared secret. + * @return The shielded AES-GCM key. */ function _hkdf(sbytes32 sharedSecret) internal view returns (sbytes32) { (bool success, bytes memory result) = address(0x68).staticcall(abi.encodePacked(bytes32(sharedSecret))); @@ -543,8 +534,11 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Seismic AES-GCM-encrypt precompile (0x66): encrypts `plaintext` under `key` / `nonce`, - * auth tag included. Reverts `PrecompileFailed`. + * @dev Calls the Seismic AES-GCM encryption precompile (0x66); reverts `PrecompileFailed` on failure. + * @param key The shielded AES-GCM key. + * @param nonce The 12-byte nonce. + * @param plaintext The data to encrypt. + * @return The ciphertext (auth tag included). */ function _aesGcmEncrypt(sbytes32 key, bytes12 nonce, bytes memory plaintext) internal view returns (bytes memory) { (bool success, bytes memory ciphertext) = address(0x66).staticcall( @@ -555,38 +549,61 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free } /** - * @dev Writes the shielded `shieldedAllowance` slot (never the inherited one); reuses `_beforeApprove`. + * @dev Sets the shielded allowance of `spender` over `account`'s tokens. + * @param account The account granting the allowance. + * @param spender The account allowed to spend on behalf of `account`. + * @param amount The shielded allowance amount. + * @param encryptEmit Whether to emit the encrypted-bytes `Approval` overload or the plaintext one. */ - function _shieldedApprove(address account, address spender, suint256 amount) internal { + function _shieldedApprove(address account, address spender, suint256 amount, bool encryptEmit) internal { uint256 amount_ = uint256(amount); _beforeApprove(account, spender, amount_); _getMYieldToOneStorageLocation().shieldedAllowance[account][spender] = amount; - emit Approval(account, spender, amount_); + if (encryptEmit) { + emit Approval(account, spender, _encryptAmount(account, spender, amount)); + } else { + emit Approval(account, spender, amount_); + } } /** - * @dev Ungated shielded balance accessor for internal use (bypasses the `balanceOf` gate). + * @dev Ungated shielded balance accessor for internal use. + * @param account The account whose balance is read. + * @return The shielded balance of `account`. */ function _balanceOf(address account) internal view returns (suint256) { return _getMYieldToOneStorageLocation().balanceOf[account]; } /** - * @dev Trusted M0 infra = the `swapFacility` immutable OR the admin-curated `allowlist`. Gates - * the native approve/transferFrom paths and the `balanceOf` read. + * @dev Returns whether `account` is trusted M0 infra: the `swapFacility` immutable or allowlisted. + * @param account The address being checked. + * @return Whether `account` is trusted infra. */ function _isInfra(address account) internal view returns (bool) { return account == swapFacility || _getMYieldToOneStorageLocation().allowlist[account]; } /** - * @dev Shielded-space override; reverts `InsufficientBalance(account, 0, amount)` — zeroed - * payload, no balance leak. The `IMExtension.InsufficientBalance` shape is unchanged. + * @dev Reverts unless `publicKey` is a 33-byte compressed secp256k1 point (`0x02`/`0x03` prefix). + * @param publicKey The public key being validated. + */ + function _revertIfInvalidPublicKey(bytes calldata publicKey) internal pure { + if (publicKey.length != 33) revert InvalidPublicKeyLength(); + if (publicKey[0] != 0x02 && publicKey[0] != 0x03) revert InvalidPublicKeyPrefix(); + } + + /** + * @dev Reverts `InsufficientBalance(account, 0, amount)` — zeroed payload, no balance leak. + * @param account The account whose shielded balance is checked. + * @param amount The amount required. */ function _revertIfInsufficientBalance(address account, uint256 amount) internal view override { + // NOTE: Branching on a shielded value leaks a 1-bit comparison via revert-vs-success; + // accepted — inherent to ERC20 insufficient-balance semantics (ssolc 10311). if (_balanceOf(account) < suint256(amount)) revert InsufficientBalance(account, 0, amount); } @@ -616,7 +633,6 @@ contract MYieldToOne is IMYieldToOne, MYieldToOneStorageLayout, MExtension, Free MYieldToOneStorageStruct storage $ = _getMYieldToOneStorageLocation(); - // Return early if the status is unchanged. if ($.allowlist[account] == status) return; $.allowlist[account] = status; diff --git a/src/projects/yieldToOne/MYieldToOneForcedTransfer.sol b/src/projects/yieldToOne/MYieldToOneForcedTransfer.sol index 42c80724..dfd45c41 100644 --- a/src/projects/yieldToOne/MYieldToOneForcedTransfer.sol +++ b/src/projects/yieldToOne/MYieldToOneForcedTransfer.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.26; +import { IERC20 } from "../../../lib/common/src/interfaces/IERC20.sol"; + import { IMYieldToOne } from "./interfaces/IMYieldToOne.sol"; import { MYieldToOne } from "./MYieldToOne.sol"; @@ -109,6 +111,16 @@ contract MYieldToOneForcedTransfer is MYieldToOne, ForcedTransferable { _setYieldRecipient(account); } + /* ============ View/Pure Functions ============ */ + + /// @inheritdoc IERC20 + /// @dev Additionally readable by FORCED_TRANSFER_MANAGER_ROLE holders (compliance must size seizures). + function balanceOf(address account) public view override returns (uint256) { + if (hasRole(FORCED_TRANSFER_MANAGER_ROLE, msg.sender)) return uint256(_balanceOf(account)); + + return super.balanceOf(account); + } + /* ============ Internal Interactive Functions ============ */ /** diff --git a/src/projects/yieldToOne/interfaces/IMYieldToOne.sol b/src/projects/yieldToOne/interfaces/IMYieldToOne.sol index 6ae031e7..6362e26b 100644 --- a/src/projects/yieldToOne/interfaces/IMYieldToOne.sol +++ b/src/projects/yieldToOne/interfaces/IMYieldToOne.sol @@ -29,31 +29,34 @@ interface IMYieldToOne { event AllowlistSet(address indexed account, bool status); /** - * @notice Emitted by user-to-user shielded transfers (the `suint256` overloads). The third field - * is an AES-GCM ciphertext of the amount, encrypted to the recipient's registered key via - * ECDH; empty bytes if the recipient has not registered one (the transfer still succeeds, - * and the amount is recoverable only via the recipient's gated `balanceOf`). - * @dev Distinct `topic0` from the inherited `Transfer(address,address,uint256)` — indexers MUST - * subscribe to both. Infra paths (mint, burn, native `transferFrom`, forced transfer) emit - * the inherited `uint256` overload exclusively. + * @notice Emitted by user-path shielded transfers (the `suint256` overloads); the amount is + * encrypted to the recipient's registered key. + * @dev Distinct `topic0` from the inherited `Transfer(address,address,uint256)` — indexers MUST track both. * @param from The address transferring the tokens. * @param to The address receiving the tokens. - * @param encryptedAmount AES-GCM ciphertext of the amount, or `bytes("")` if `to` is unregistered. + * @param encryptedAmount AES-GCM ciphertext of the amount; empty bytes if `to` has no registered key. */ event Transfer(address indexed from, address indexed to, bytes encryptedAmount); /** - * @notice Emitted by `setContractKey` once the contract keypair is installed. Only the - * public key is logged; the private key is held in shielded storage and is - * never observable from logs or events. + * @notice Emitted by user-path shielded approvals (the `suint256` overload); the allowance is + * encrypted to the spender's registered key. + * @dev Distinct `topic0` from the inherited `Approval(address,address,uint256)` — indexers MUST track both. + * @param account The account granting the allowance. + * @param spender The account allowed to spend on behalf of `account`. + * @param encryptedAmount AES-GCM ciphertext of the allowance; empty bytes if `spender` has no registered key. + */ + event Approval(address indexed account, address indexed spender, bytes encryptedAmount); + + /** + * @notice Emitted when the contract's encrypted-event keypair is installed; only the public key is logged. * @param publicKey The contract's compressed (33-byte) secp256k1 public key. */ event ContractKeySet(bytes publicKey); /** - * @notice Emitted by `registerPublicKey` when an account installs or overwrites its - * recipient public key for encrypted-event decryption. - * @param account The account whose recipient public key was (re-)registered. + * @notice Emitted when an account registers (or overwrites) its public key for encrypted-event decryption. + * @param account The account whose public key was registered. */ event PublicKeyRegistered(address indexed account); @@ -68,40 +71,39 @@ interface IMYieldToOne { /// @notice Emitted in initializer if Admin is 0x0. error ZeroAdmin(); - /// @notice Emitted when a public read accesses a shielded value without holder authorization - /// (caller must use a Seismic signed read with `msg.sender == account`). + /// @notice Emitted when a gated read (`balanceOf` / `allowance`) is called by an unauthorized account. error Unauthorized(); - /// @notice Reverted when native `IERC20.approve` is called with a non-allowlisted spender, or - /// when the `permit` path is invoked. Holders must use the shielded - /// `approve(address,suint256)` overload, or approve an allowlisted infra address. + /// @notice Emitted when the native `approve` is called with a non-infra spender, or `permit` is + /// called; use the shielded `approve(address,suint256)` overload instead. error UseShieldedApprove(); - /// @notice Reverted when the native `IERC20.transfer` path is invoked, or when native - /// `IERC20.transferFrom` is called by a non-allowlisted caller. Callers must use the - /// shielded overloads; only allowlisted infra may use the native `transferFrom` path. + /// @notice Emitted when the native `transfer` is called, or the native `transferFrom` is called + /// by a non-infra caller; use the shielded `suint256` overloads instead. error UseShieldedTransfer(); - /// @notice Reverted in `setAllowlisted` if the account is the zero address. + /// @notice Emitted in `setAllowlisted` if the account is 0x0. error ZeroAllowlistAccount(); - /// @notice Reverted by `setContractKey` / `registerPublicKey` if the supplied public - /// key is not exactly 33 bytes (compressed secp256k1 encoding). + /// @notice Emitted in `setContractKey` and `registerPublicKey` if the public key is not 33 bytes. error InvalidPublicKeyLength(); - /// @notice Reverted by `setContractKey` if the contract keypair has already been - /// installed. Rotation is deliberately not supported — a new key would orphan - /// every historical ciphertext. + /// @notice Emitted in `setContractKey` and `registerPublicKey` if the public key prefix is not `0x02`/`0x03`. + error InvalidPublicKeyPrefix(); + + /// @notice Emitted in `setContractKey` if the private key is zero. + error ZeroPrivateKey(); + + /// @notice Emitted in `setContractKey` if the contract keypair is already installed. error ContractKeyAlreadySet(); - /// @notice Reverted by the encrypted-emit path if a shielded transfer is attempted - /// before the admin has called `setContractKey`. + /// @notice Emitted by the encrypted-event path if the contract keypair has not been installed. error ContractKeyNotSet(); - /// @notice Reverted by an encrypted-emit precompile wrapper when the underlying - /// `staticcall` to the Seismic precompile fails. `precompile` is the address - /// of the precompile that returned a failing result (0x65 ECDH, 0x66 AES-GCM, - /// 0x68 HKDF). + /** + * @notice Emitted when a Seismic precompile staticcall fails. + * @param precompile The failing precompile address (0x65 ECDH, 0x66 AES-GCM, 0x68 HKDF). + */ error PrecompileFailed(address precompile); /* ============ Interactive Functions ============ */ @@ -121,12 +123,8 @@ interface IMYieldToOne { /** * @notice Adds or removes `account` from the infra allowlist. * @dev MUST only be callable by the DEFAULT_ADMIN_ROLE. - * @dev Allowlisted addresses MUST be audited M0 infrastructure contracts (e.g. Portal, - * LimitOrderProtocol) — never EOAs or contracts that re-expose `balanceOf`. (SwapFacility - * is permanently exempt via the immutable and does not need allowlisting.) An - * allowlisted address may use the native `uint256` `approve` - * (as spender) and `transferFrom` (as caller) paths and may read any holder's cleartext - * `balanceOf`. + * @dev Allowlisted addresses MUST be audited M0 infra contracts, never EOAs or contracts re-exposing `balanceOf`. + * @dev Grants native `approve` (as spender) and `transferFrom` (as caller) paths and ungated `balanceOf` reads. * @dev SHOULD revert if `account` is 0x0. SHOULD return early if the status is unchanged. * @param account The address whose allowlist status is being set. * @param status The new allowlist status (`true` = allowlisted). @@ -143,58 +141,48 @@ interface IMYieldToOne { function setAllowlisted(address[] calldata accounts, bool status) external; /** - * @notice Shielded ERC20 transfer. `amount` is stored and compared in shielded space. Emits the - * encrypted-bytes `Transfer(address,address,bytes)` overload to `recipient`'s registered - * key (empty ciphertext if `recipient` has not registered one — transfer still succeeds). + * @notice Shielded ERC20 transfer of `amount` tokens to `recipient`. + * @dev Emits the encrypted-bytes `Transfer(address,address,bytes)` overload. * @param recipient The address receiving the tokens. * @param amount The shielded amount to transfer. - * @return success Always `true` on non-revert (mirrors `IERC20.transfer`). + * @return Whether or not the transfer was successful. */ function transfer(address recipient, suint256 amount) external returns (bool); /** - * @notice Shielded ERC20 approve. Stores the allowance as `suint256`. + * @notice Shielded ERC20 approval of `spender` for `amount` of the caller's tokens. + * @dev Emits the encrypted-bytes `Approval(address,address,bytes)` overload. * @param spender The address allowed to spend on behalf of `msg.sender`. - * @param amount The shielded allowance amount. Use `suint256(type(uint256).max)` for an - * infinite, non-decrementing allowance (matches `ERC20ExtendedUpgradeable`). - * @return success Always `true` on non-revert. + * @param amount The shielded allowance; `suint256(type(uint256).max)` is an infinite, non-decrementing allowance. + * @return Whether or not the approval was successful. */ function approve(address spender, suint256 amount) external returns (bool); /** - * @notice Shielded ERC20 transferFrom. Reads and decrements the shielded allowance in shielded - * space; reverts `InsufficientAllowance(spender, 0, amount)` (zeroed payload). Emits the - * encrypted-bytes `Transfer` overload (empty ciphertext if `recipient` is unregistered). + * @notice Shielded ERC20 transferFrom; reads and decrements the allowance in shielded space. + * @dev Emits the encrypted-bytes `Transfer(address,address,bytes)` overload. * @param sender The address whose tokens are being moved. * @param recipient The address receiving the tokens. * @param amount The shielded amount to transfer. - * @return success Always `true` on non-revert. + * @return Whether or not the transfer was successful. */ function transferFrom(address sender, address recipient, suint256 amount) external returns (bool); /** - * @notice Installs the contract's encryption keypair used to derive per-recipient - * AES-GCM keys for shielded `Transfer` event payloads. One-shot: reverts - * `ContractKeyAlreadySet` on any subsequent call. - * @dev MUST only be callable by the `DEFAULT_ADMIN_ROLE`. - * @dev MUST be sent as a Seismic `TxSeismic` transaction (type `0x4A`) so the - * private key is encrypted in calldata. This is an operational requirement - * that cannot be enforced from Solidity. - * @dev Reverts `InvalidPublicKeyLength` unless `publicKey.length == 33` - * (compressed secp256k1 encoding). - * @dev Rotation is intentionally out of scope: rotating the contract key would - * orphan every historical ciphertext stored in past events. - * @param privateKey The contract's secp256k1 private key, shielded at the ABI - * boundary so it remains in flagged storage. + * @notice Installs the contract's encrypted-event keypair; one-shot, reverts `ContractKeyAlreadySet` + * on any subsequent call. Until installed, user-path shielded transfers and approvals + * revert `ContractKeyNotSet`. + * @dev MUST only be callable by the DEFAULT_ADMIN_ROLE. + * @dev MUST be sent as a `TxSeismic` (type 0x4A) transaction — plain calldata would leak the private key. + * @dev Rotation is unsupported: a new contract key would orphan every historical ciphertext. + * @param privateKey The contract's secp256k1 private key, shielded at the ABI boundary. * @param publicKey The contract's compressed (33-byte) secp256k1 public key. */ function setContractKey(sbytes32 privateKey, bytes calldata publicKey) external; /** - * @notice Registers the caller's recipient public key. Idempotent — a subsequent call - * overwrites the previously registered key (future ciphertexts use the new - * key; historical ciphertexts remain decryptable only with the old key). - * @dev Reverts `InvalidPublicKeyLength` unless `publicKey.length == 33`. + * @notice Registers the caller's public key for encrypted-event payloads; a subsequent call + * overwrites the previously registered key. * @param publicKey The caller's compressed (33-byte) secp256k1 public key. */ function registerPublicKey(bytes calldata publicKey) external; @@ -218,18 +206,15 @@ interface IMYieldToOne { function isAllowlisted(address account) external view returns (bool); /** - * @notice Returns the recipient public key previously registered by `account` via - * `registerPublicKey`, or empty bytes if none has been registered. Plain - * (non-shielded) read — readable by any caller. + * @notice Returns the public key registered by `account`, or empty bytes if none; readable by any caller. * @param account The address whose registered public key is being queried. * @return The registered compressed (33-byte) secp256k1 public key, or empty bytes. */ function publicKeyOf(address account) external view returns (bytes memory); /** - * @notice Returns the contract's currently installed public key, or empty bytes if - * `setContractKey` has not yet been called. Plain (non-shielded) read — - * off-chain decryption clients fetch this to verify the ECDH peer. + * @notice Returns the contract's installed public key, or empty bytes if `setContractKey` has not + * been called; readable by any caller. * @return The contract's compressed (33-byte) secp256k1 public key, or empty bytes. */ function contractPublicKey() external view returns (bytes memory); diff --git a/test/harness/JMIExtensionHarness.sol b/test/harness/JMIExtensionHarness.sol index 78116c88..fa040347 100644 --- a/test/harness/JMIExtensionHarness.sol +++ b/test/harness/JMIExtensionHarness.sol @@ -38,6 +38,16 @@ contract JMIExtensionHarness is JMIExtension { _getMYieldToOneStorageLocation().balanceOf[account] = suint256(amount); } + /// @dev Bypasses the public `balanceOf` gate. + function getBalanceOf(address account) external view returns (uint256) { + return uint256(_getMYieldToOneStorageLocation().balanceOf[account]); + } + + /// @dev Reads the encrypted-event nonce counter. + function getEncryptedEventNonce() external view returns (uint256) { + return _getMYieldToOneStorageLocation().encryptedEventNonce; + } + function setTotalAssets(uint256 amount) external { _getJMIExtensionStorageLocation().totalAssets = amount; } diff --git a/test/harness/MYieldToOneForcedTransferHarness.sol b/test/harness/MYieldToOneForcedTransferHarness.sol index c300414a..4f045d15 100644 --- a/test/harness/MYieldToOneForcedTransferHarness.sol +++ b/test/harness/MYieldToOneForcedTransferHarness.sol @@ -34,7 +34,7 @@ contract MYieldToOneForcedTransferHarness is MYieldToOneForcedTransfer { _getMYieldToOneStorageLocation().balanceOf[account] = suint256(amount); } - /// @dev Bypasses the public `balanceOf` gate — for test assertions only. + /// @dev Bypasses the public `balanceOf` gate. function getBalanceOf(address account) external view returns (uint256) { return uint256(_getMYieldToOneStorageLocation().balanceOf[account]); } @@ -43,8 +43,13 @@ contract MYieldToOneForcedTransferHarness is MYieldToOneForcedTransfer { _getMYieldToOneStorageLocation().shieldedAllowance[owner][spender] = suint256(amount); } - /// @dev Bypasses the `shieldedAllowance` gate — for test assertions only. + /// @dev Bypasses the gated `allowance` read. function getShieldedAllowance(address owner, address spender) external view returns (uint256) { return uint256(_getMYieldToOneStorageLocation().shieldedAllowance[owner][spender]); } + + /// @dev Reads the encrypted-event nonce counter. + function getEncryptedEventNonce() external view returns (uint256) { + return _getMYieldToOneStorageLocation().encryptedEventNonce; + } } diff --git a/test/harness/MYieldToOneHarness.sol b/test/harness/MYieldToOneHarness.sol index c3e69613..b6bc0e2d 100644 --- a/test/harness/MYieldToOneHarness.sol +++ b/test/harness/MYieldToOneHarness.sol @@ -24,7 +24,7 @@ contract MYieldToOneHarness is MYieldToOne { _getMYieldToOneStorageLocation().balanceOf[account] = suint256(amount); } - /// @dev Bypasses the public `balanceOf` gate — for test assertions only. + /// @dev Bypasses the public `balanceOf` gate. function getBalanceOf(address account) external view returns (uint256) { return uint256(_getMYieldToOneStorageLocation().balanceOf[account]); } @@ -37,12 +37,12 @@ contract MYieldToOneHarness is MYieldToOne { _getMYieldToOneStorageLocation().shieldedAllowance[owner][spender] = suint256(amount); } - /// @dev Bypasses the `shieldedAllowance` gate — for test assertions only. + /// @dev Bypasses the gated `allowance` read. function getShieldedAllowance(address owner, address spender) external view returns (uint256) { return uint256(_getMYieldToOneStorageLocation().shieldedAllowance[owner][spender]); } - /// @dev Reads the monotonic encrypted-event nonce counter (slot 8) — for test assertions only. + /// @dev Reads the encrypted-event nonce counter. function getEncryptedEventNonce() external view returns (uint256) { return _getMYieldToOneStorageLocation().encryptedEventNonce; } diff --git a/test/integration/MExtensionSystem.t.sol b/test/integration/MExtensionSystem.t.sol index 088ef427..316dd749 100644 --- a/test/integration/MExtensionSystem.t.sol +++ b/test/integration/MExtensionSystem.t.sol @@ -132,6 +132,10 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { swapFacility.grantRole(M_SWAPPER_ROLE, alice); swapFacility.grantRole(M_SWAPPER_ROLE, bob); swapFacility.grantRole(M_SWAPPER_ROLE, carol); + mYieldToOne.setContractKey( + sbytes32(bytes32(uint256(0xC0FFEE))), + hex"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); vm.stopPrank(); } @@ -188,7 +192,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapFacility.swap(address(mYieldFee), address(mYieldToOne), mYieldFeeBalance - 2, alice); - uint256 mYieldToOneBalance = mYieldToOne.balanceOf(alice); + uint256 mYieldToOneBalance = mYieldToOne.getBalanceOf(alice); assertEq(mYieldToOneBalance, 10e6 - 2); vm.prank(alice); @@ -231,7 +235,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapFacility.swap(address(mYieldFee), address(mYieldToOne), mYieldFeeBalance, alice); - uint256 mYieldToOneBalance = mYieldToOne.balanceOf(alice); + uint256 mYieldToOneBalance = mYieldToOne.getBalanceOf(alice); assertEq(mYieldToOneBalance, 10e6); // fast forward to accrue yield @@ -284,7 +288,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { mYieldToOne.claimYield(); - mYieldToOneBalance = mYieldToOne.balanceOf(yieldRecipient); + mYieldToOneBalance = mYieldToOne.getBalanceOf(yieldRecipient); assertEq(mYieldToOneBalance, 11401, "yield recipient should have its yield claimed"); @@ -364,7 +368,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { swapFacility.swap(extensions[extensionIndex], address(mToken), amount, alice); mYieldToOne.claimYield(); - assertApproxEqAbs(mYieldToOne.balanceOf(yieldRecipient), yields[M_YIELD_TO_ONE], 20); + assertApproxEqAbs(mYieldToOne.getBalanceOf(yieldRecipient), yields[M_YIELD_TO_ONE], 20); mYieldFee.claimYieldFor(alice); assertApproxEqAbs(mYieldFee.balanceOf(alice), yields[M_YIELD_FEE], 50); @@ -508,7 +512,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapFacility.swap(address(mEarnerManager), address(mYieldToOne), 10e6 - 2, alice); - assertEq(mYieldToOne.balanceOf(alice), 10e6, "mYieldToOne balance should be 10e6"); + assertEq(mYieldToOne.getBalanceOf(alice), 10e6, "mYieldToOne balance should be 10e6"); vm.expectRevert(abi.encodeWithSelector(ISwapFacility.PermissionedExtension.selector, address(mYieldFee))); @@ -530,6 +534,9 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(earnerManager); mEarnerManager.setAccountInfo(address(swapAdapter), true, 0); + vm.prank(admin); + mYieldToOne.setAllowlisted(address(swapAdapter), true); + vm.startPrank(alice); // Approve swap adapter for all tokens @@ -554,7 +561,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapFacility.swap(address(mToken), address(mYieldToOne), 10e6, alice); - assertEq(mYieldToOne.balanceOf(alice), 10e6, "mYieldToOne balance should be 10e6"); + assertEq(mYieldToOne.getBalanceOf(alice), 10e6, "mYieldToOne balance should be 10e6"); vm.prank(alice); swapAdapter.swapOut(address(mYieldToOne), 10e6 - 2, USDC, 0, alice, ""); @@ -566,7 +573,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapAdapter.swapIn(USDC, usdcBalance, address(mYieldToOne), 0, alice, ""); - uint256 yieldToOneBalance = mYieldToOne.balanceOf(alice); + uint256 yieldToOneBalance = mYieldToOne.getBalanceOf(alice); assertEq(yieldToOneBalance, 9997997, "mYieldToOne balance of alice should be 10e6"); @@ -609,7 +616,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.stopPrank(); vm.prank(alice); - mYieldToOne.approve(bob, 10e6); + mYieldToOne.approve(bob, suint256(10e6)); vm.prank(bob); mToken.approve(address(swapFacility), 10e6); @@ -617,7 +624,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { vm.prank(alice); swapFacility.swap(address(mToken), address(mYieldToOne), 10e6, alice); - uint256 mYieldToOneBalance = mYieldToOne.balanceOf(alice); + uint256 mYieldToOneBalance = mYieldToOne.getBalanceOf(alice); assertEq(mYieldToOneBalance, 10e6, "mYieldToOneBalance should be 10e6"); @@ -648,7 +655,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { mYieldToOne.claimYield(); assertEq( - mYieldToOne.balanceOf(yieldRecipient), + mYieldToOne.getBalanceOf(yieldRecipient), mBalanceAfter - mBalanceBefore - 2, "yield should be claimed to yield recipient" ); @@ -660,17 +667,17 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { swapFacility.swap(address(mYieldToOne), address(mYieldFee), 10e6 - 2, alice); vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); - mYieldToOne.transfer(bob, 10e6); + mYieldToOne.transfer(bob, suint256(10e6)); vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); - mYieldToOne.approve(bob, 10e6); + mYieldToOne.approve(bob, suint256(10e6)); vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); - mYieldToOne.transferFrom(alice, bob, 10e6); + mYieldToOne.transferFrom(alice, bob, suint256(10e6)); vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); swapFacility.swap(address(mToken), address(mYieldToOne), 10e6, alice); @@ -702,7 +709,7 @@ contract MExtensionSystemIntegrationTests is BaseIntegrationTest { swapFacility.swap(address(mToken), address(mYieldToOne), 10e6, bob); vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); - mYieldToOne.transfer(alice, 10e6); + mYieldToOne.transfer(alice, suint256(10e6)); } function test_mEarnerManager_whitelistManagement_withActivePositions() public { diff --git a/test/integration/MYieldToOne.t.sol b/test/integration/MYieldToOne.t.sol index 1e8fd459..34790e25 100644 --- a/test/integration/MYieldToOne.t.sol +++ b/test/integration/MYieldToOne.t.sol @@ -42,6 +42,12 @@ contract MYieldToOneIntegrationTests is BaseIntegrationTest { mExtensionDeployOptions ) ); + + vm.prank(admin); + mYieldToOne.setContractKey( + sbytes32(bytes32(uint256(0xC0FFEE))), + hex"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); } function test_integration_constants() external view { @@ -89,7 +95,7 @@ contract MYieldToOneIntegrationTests is BaseIntegrationTest { // transfers do not affect yield vm.prank(alice); - mYieldToOne.transfer(bob, amount / 2); + mYieldToOne.transfer(bob, suint256(amount / 2)); assertEq(mYieldToOne.getBalanceOf(bob), amount / 2); assertEq(mYieldToOne.getBalanceOf(alice), amount / 2); @@ -269,8 +275,7 @@ contract MYieldToOneIntegrationTests is BaseIntegrationTest { } function test_unwrapWithPermits() external { - // `permit` reverts, so the `swapWithPermit` path is unsupported here. Migrated to the native - // `approve(swapFacility, amount)` path (swapFacility is infra), then a non-permit swap-out. + // `permit` reverts on MYieldToOne, so this swaps out via the native infra `approve` path. _addToList(EARNERS_LIST, address(mYieldToOne)); mYieldToOne.enableEarning(); @@ -293,9 +298,7 @@ contract MYieldToOneIntegrationTests is BaseIntegrationTest { } function test_unwrapWithPermits_unsupported() external { - // The `swapWithPermit` swap-out is explicitly unsupported for MYieldToOne: `permit` reverts, - // the revert is swallowed by SwapFacility's try/catch, the shielded allowance stays zero, and - // the subsequent native `transferFrom` (SwapFacility is infra) reverts on zero allowance. + // `permit` reverts inside SwapFacility's try/catch, so the swap fails on the zero allowance. _addToList(EARNERS_LIST, address(mYieldToOne)); mYieldToOne.enableEarning(); diff --git a/test/integration/seismic/MExtensionSystemSeismic.t.sol b/test/integration/seismic/MExtensionSystemSeismic.t.sol new file mode 100644 index 00000000..6c5dba88 --- /dev/null +++ b/test/integration/seismic/MExtensionSystemSeismic.t.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +import { Vm } from "../../../lib/forge-std/src/Vm.sol"; + +import { Upgrades } from "../../../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; + +import { IERC20 } from "../../../lib/common/src/interfaces/IERC20.sol"; + +import { MYieldToOneForcedTransfer } from "../../../src/projects/yieldToOne/MYieldToOneForcedTransfer.sol"; +import { MYieldFee } from "../../../src/projects/yieldToAllWithFee/MYieldFee.sol"; + +import { IMYieldToOne } from "../../../src/projects/yieldToOne/interfaces/IMYieldToOne.sol"; +import { IMExtension } from "../../../src/interfaces/IMExtension.sol"; +import { IFreezable } from "../../../src/components/freezable/IFreezable.sol"; +import { ISwapFacility } from "../../../src/swap/interfaces/ISwapFacility.sol"; + +import { MYieldToOneForcedTransferHarness } from "../../harness/MYieldToOneForcedTransferHarness.sol"; +import { MYieldFeeHarness } from "../../harness/MYieldFeeHarness.sol"; + +import { BaseUnitTest } from "../../utils/BaseUnitTest.sol"; + +/// @dev In-process, NON-FORKING system integration suite for the shielded SRC-20 token. +/// Deploys the shielded `MYieldToOneForcedTransfer` and a sibling `MYieldFee` behind proxies +/// against the same real `SwapFacility` + `MockM` infra the unit suite uses. Runs against the +/// REAL Seismic precompiles (sforge's mercury EVM) — a real contract key is installed and holder +/// public keys are registered, so the encrypted-event path is exercised with no `vm.mockCall`. +/// +/// Mirrors the SRC-20-relevant flows of the excluded mainnet-fork `MExtensionSystem.t.sol` +/// (multi-extension swap, yield lifecycle, freeze-during-yield, permissioned gating) with +/// RELATIONAL assertions — conservation, monotonicity, typed reverts — not the fork suite's +/// hardcoded mainnet yield constants. +contract MExtensionSystemSeismicIntegrationTests is BaseUnitTest { + bytes32 internal constant TRANSFER_BYTES_TOPIC = keccak256("Transfer(address,address,bytes)"); + + // Non-round index so the sibling extension's principal math runs on a realistic value. + uint128 internal constant _M_INDEX = 1_100000068703; + + MYieldToOneForcedTransferHarness public mYieldToOne; + MYieldFeeHarness public mYieldFee; + + Vm.Wallet public contractWallet; + Vm.Wallet public aliceWallet; + Vm.Wallet public bobWallet; + + function setUp() public override { + super.setUp(); + + // Realistic, non-zero $M index: the sibling `MYieldFee` principal conversions divide by it. + mToken.setCurrentIndex(_M_INDEX); + + // Deploy the shielded extension behind a proxy. Args are passed in PRODUCTION + // `MYieldToOneForcedTransfer.initialize` order (yieldRecipient, admin, freezeManager, + // yieldRecipientManager, pauser, forcedTransferManager): the harness's positional forward to + // `super.initialize` makes the param NAMES on its own `initialize` misleading, so role wiring + // is correct only when the call site mirrors the production order — same as MYieldToOneSimulation.t.sol. + mYieldToOne = MYieldToOneForcedTransferHarness( + Upgrades.deployTransparentProxy( + "MYieldToOneForcedTransferHarness.sol:MYieldToOneForcedTransferHarness", + admin, + abi.encodeWithSelector( + MYieldToOneForcedTransfer.initialize.selector, + "Seismic Dollar", + "USDS", + yieldRecipient, + admin, + freezeManager, + yieldRecipientManager, + pauser, + forcedTransferManager + ), + mExtensionDeployOptions + ) + ); + + // Sibling extension. `MYieldFee` is index-based; with earning disabled its index stays at + // EXP_SCALED_ONE so swaps move value 1:1, which keeps the cross-extension assertions clean. + mYieldFee = MYieldFeeHarness( + Upgrades.deployTransparentProxy( + "MYieldFeeHarness.sol:MYieldFeeHarness", + admin, + abi.encodeWithSelector( + MYieldFeeHarness.initialize.selector, + "Seismic Yield", + "SY", + uint16(0), + feeRecipient, + admin, + feeManager, + claimRecipientManager, + freezeManager, + pauser + ), + mExtensionDeployOptions + ) + ); + + registrar.setEarner(address(mYieldToOne), true); + registrar.setEarner(address(mYieldFee), true); + + // Real Seismic keys: contract keypair + per-holder registered pubkeys drive the real + // encrypted-event precompiles (0x65/0x66/0x68), no mocks. + contractWallet = vm.createWallet("seismic contract key"); + aliceWallet = vm.createWallet("seismic alice key"); + bobWallet = vm.createWallet("seismic bob key"); + + vm.prank(admin); + mYieldToOne.setContractKey(sbytes32(bytes32(contractWallet.privateKey)), _compressed(contractWallet)); + + vm.prank(alice); + mYieldToOne.registerPublicKey(_compressed(aliceWallet)); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_compressed(bobWallet)); + + vm.startPrank(admin); + swapFacility.grantRole(M_SWAPPER_ROLE, alice); + swapFacility.grantRole(M_SWAPPER_ROLE, bob); + vm.stopPrank(); + } + + /* ============ wrap -> shielded balance ============ */ + + function test_wrap_shieldedBalanceAndBacking() external { + uint256 amount_ = 1_000e6; + + _wrapInto(mYieldToOne, alice, amount_); + + // Wrap is 1:1: the holder's shielded balance equals the wrapped amount. + assertEq(mYieldToOne.getBalanceOf(alice), amount_); + assertEq(mYieldToOne.totalSupply(), amount_); + + // M backing fully covers the minted supply. + assertEq(mToken.balanceOf(address(mYieldToOne)), mYieldToOne.totalSupply()); + } + + function testFuzz_wrap_shieldedBalanceAndBacking(uint256 amount_) external { + amount_ = bound(amount_, 1, 1e15); + + _wrapInto(mYieldToOne, alice, amount_); + + assertEq(mYieldToOne.getBalanceOf(alice), amount_); + assertEq(mYieldToOne.totalSupply(), amount_); + assertGe(mToken.balanceOf(address(mYieldToOne)), mYieldToOne.totalSupply()); + } + + /* ============ cross-extension swap (native infra paths) ============ */ + + function test_crossExtensionSwap_mYieldToOne_to_mYieldFee() external { + uint256 amount_ = 500e6; + + _wrapInto(mYieldToOne, alice, amount_); + + uint256 totalBackingBefore_ = mToken.balanceOf(address(mYieldToOne)) + mToken.balanceOf(address(mYieldFee)); + + // The swap-out leg routes through MYieldToOne's NATIVE infra paths: the holder's + // `approve(swapFacility, amount)` and SwapFacility's `transferFrom(holder, facility, amount)` + // both take the `_isInfra` branch (swapFacility is the immutable infra address), NOT the + // user-revert `UseShieldedApprove` / `UseShieldedTransfer` paths. + vm.prank(alice); + mYieldToOne.approve(address(swapFacility), amount_); + + vm.prank(alice); + swapFacility.swap(address(mYieldToOne), address(mYieldFee), amount_, alice); + + // Value conserved across the hop (1:1, both extensions at a flat index). + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldFee.balanceOf(alice), amount_); + + // M backing is conserved system-wide, only relocated between the two extensions. + assertEq(mToken.balanceOf(address(mYieldToOne)) + mToken.balanceOf(address(mYieldFee)), totalBackingBefore_); + assertEq(mToken.balanceOf(address(mYieldFee)), amount_); + assertEq(mToken.balanceOf(address(mYieldToOne)), 0); + } + + function test_crossExtensionSwap_nativeApproveReachesInfraPath() external { + uint256 amount_ = 10e6; + + _wrapInto(mYieldToOne, alice, amount_); + + // A non-infra spender on the native overload must hit the user-revert path... + vm.prank(alice); + vm.expectRevert(IMYieldToOne.UseShieldedApprove.selector); + mYieldToOne.approve(bob, amount_); + + // ...while the infra spender (swapFacility) is accepted and records the allowance. + vm.prank(alice); + mYieldToOne.approve(address(swapFacility), amount_); + + vm.prank(alice); + assertEq(mYieldToOne.allowance(alice, address(swapFacility)), amount_); + } + + /* ============ shielded transfer / transferFrom between holders ============ */ + + function test_shieldedTransfer_emitsRealCiphertext_andMovesBalance() external { + uint256 amount_ = 800e6; + + _wrapInto(mYieldToOne, alice, amount_); + + uint256 nonceBefore_ = mYieldToOne.getEncryptedEventNonce(); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount_)); + + // A real encrypted `Transfer(address,address,bytes)` is emitted to the registered recipient, + // with non-empty ciphertext, and the per-emit nonce advances. + bytes memory ciphertext_ = _extractPayload(TRANSFER_BYTES_TOPIC, alice, bob); + assertGt(ciphertext_.length, 0); + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore_ + 1); + + // Balances move correctly under the shielded path. + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), amount_); + assertEq(mYieldToOne.totalSupply(), amount_); + } + + function test_shieldedTransferFrom_betweenHolders() external { + uint256 amount_ = 300e6; + + _wrapInto(mYieldToOne, alice, amount_); + + // alice approves carol (shielded) to move funds to the registered recipient bob. + vm.prank(alice); + mYieldToOne.approve(carol, suint256(amount_)); + + uint256 nonceBefore_ = mYieldToOne.getEncryptedEventNonce(); + + vm.recordLogs(); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(amount_)); + + bytes memory ciphertext_ = _extractPayload(TRANSFER_BYTES_TOPIC, alice, bob); + assertGt(ciphertext_.length, 0); + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore_ + 1); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), amount_); + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), 0); + } + + function test_shieldedTransfer_insufficientBalance() external { + uint256 amount_ = 100e6; + + _wrapInto(mYieldToOne, alice, amount_); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(IMExtension.InsufficientBalance.selector, alice, 0, amount_ + 1)); + mYieldToOne.transfer(bob, suint256(amount_ + 1)); + } + + /* ============ yield lifecycle ============ */ + + function test_yieldLifecycle_accrueAndClaim() external { + uint256 amount_ = 1_000e6; + + _wrapInto(mYieldToOne, alice, amount_); + + assertEq(mYieldToOne.yield(), 0); + assertGe(mToken.balanceOf(address(mYieldToOne)), mYieldToOne.totalSupply()); + + // Simulate M yield by bumping the extension's mock M balance (the MockM has no index accrual). + uint256 yieldDelta_ = 7_500; + mToken.setBalanceOf(address(mYieldToOne), mToken.balanceOf(address(mYieldToOne)) + yieldDelta_); + + assertGt(mYieldToOne.yield(), 0); + assertEq(mYieldToOne.yield(), yieldDelta_); + + uint256 recipientBefore_ = mYieldToOne.getBalanceOf(yieldRecipient); + uint256 supplyBefore_ = mYieldToOne.totalSupply(); + + uint256 claimed_ = mYieldToOne.claimYield(); + + // claimYield mints the accrued yield to the yield recipient and clears the surplus. + assertEq(claimed_, yieldDelta_); + assertEq(mYieldToOne.getBalanceOf(yieldRecipient), recipientBefore_ + yieldDelta_); + assertEq(mYieldToOne.totalSupply(), supplyBefore_ + yieldDelta_); + assertEq(mYieldToOne.yield(), 0); + + // Backing invariant holds throughout. + assertGe(mToken.balanceOf(address(mYieldToOne)), mYieldToOne.totalSupply()); + } + + function testFuzz_yieldLifecycle_backingHolds(uint256 amount_, uint256 yieldDelta_) external { + amount_ = bound(amount_, 1, 1e15); + yieldDelta_ = bound(yieldDelta_, 1, 1e15); + + _wrapInto(mYieldToOne, alice, amount_); + + mToken.setBalanceOf(address(mYieldToOne), mToken.balanceOf(address(mYieldToOne)) + yieldDelta_); + + assertEq(mYieldToOne.yield(), yieldDelta_); + + mYieldToOne.claimYield(); + + assertEq(mYieldToOne.yield(), 0); + assertEq(mYieldToOne.getBalanceOf(yieldRecipient), yieldDelta_); + assertGe(mToken.balanceOf(address(mYieldToOne)), mYieldToOne.totalSupply()); + } + + /* ============ freeze during yield ============ */ + + function test_freeze_duringYield_blocksFlowAndSeizes() external { + uint256 amount_ = 1_000e6; + + _wrapInto(mYieldToOne, alice, amount_); + + // Grant the swap-out and spender allowances BEFORE freezing, so the blocked operations + // surface `AccountFrozen` from the `_beforeTransfer` hook rather than tripping the + // allowance check first. + vm.prank(alice); + mYieldToOne.approve(address(swapFacility), amount_); + + vm.prank(alice); + mYieldToOne.approve(carol, suint256(amount_)); + + // Accrue some yield before freezing. + mToken.setBalanceOf(address(mYieldToOne), mToken.balanceOf(address(mYieldToOne)) + 5_000); + uint256 yieldBeforeFreeze_ = mYieldToOne.yield(); + assertGt(yieldBeforeFreeze_, 0); + + vm.expectEmit(true, true, true, true); + emit IFreezable.Frozen(alice, vm.getBlockTimestamp()); + + vm.prank(freezeManager); + mYieldToOne.freeze(alice); + + // Yield keeps accruing while alice is frozen. + mToken.setBalanceOf(address(mYieldToOne), mToken.balanceOf(address(mYieldToOne)) + 5_000); + assertGt(mYieldToOne.yield(), yieldBeforeFreeze_); + + // A frozen holder's swap/transfer/approve/transferFrom all revert AccountFrozen. + vm.prank(alice); + mToken.approve(address(swapFacility), amount_); // mock M approval is unaffected by freeze + + vm.startPrank(alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + swapFacility.swap(address(mYieldToOne), address(mYieldFee), amount_, alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + mYieldToOne.transfer(bob, suint256(amount_)); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + mYieldToOne.approve(address(swapFacility), amount_); + + vm.stopPrank(); + + // transferFrom on a frozen owner also reverts (here carol would be the spender). + vm.prank(carol); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + mYieldToOne.transferFrom(alice, bob, suint256(amount_)); + + // The forced-transfer manager seizes from the frozen account (no freeze checks on this path). + uint256 seizeAmount_ = 400e6; + vm.prank(forcedTransferManager); + mYieldToOne.forceTransfer(alice, bob, seizeAmount_); + + assertEq(mYieldToOne.getBalanceOf(alice), amount_ - seizeAmount_); + assertEq(mYieldToOne.getBalanceOf(bob), seizeAmount_); + + // Unfreeze restores normal flow: alice can transfer the remainder. + vm.prank(freezeManager); + mYieldToOne.unfreeze(alice); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount_ - seizeAmount_)); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), amount_); + + // Supply unchanged by the freeze/seize/transfer churn; yield still claimable. + assertEq(mYieldToOne.totalSupply(), amount_); + assertGt(mYieldToOne.yield(), 0); + } + + /* ============ permissioned-extension gating ============ */ + + function test_permissionedExtension_gating() external { + uint256 amount_ = 100e6; + + // Seed alice's $M and a standing $M approval to the facility (the swap-out leg returns it). + mToken.setBalanceOf(alice, amount_); + + vm.prank(alice); + mToken.approve(address(swapFacility), amount_); + + // Mark MYieldToOne permissioned: only an explicitly-allowed M swapper may swap M in/out of it. + vm.prank(admin); + swapFacility.setPermissionedExtension(address(mYieldToOne), true); + + // alice holds M_SWAPPER_ROLE but is NOT yet a permissioned M swapper for this extension. + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(ISwapFacility.NotApprovedPermissionedSwapper.selector, address(mYieldToOne), alice) + ); + swapFacility.swapInM(address(mYieldToOne), amount_, alice); + + // Grant the permissioned-swapper right; the swap-in now succeeds. + vm.prank(admin); + swapFacility.setPermissionedMSwapper(address(mYieldToOne), alice, true); + + vm.prank(alice); + swapFacility.swapInM(address(mYieldToOne), amount_, alice); + assertEq(mYieldToOne.getBalanceOf(alice), amount_); + + // A permissioned extension cannot be the input of an extension->extension swap. + vm.prank(alice); + mYieldToOne.approve(address(swapFacility), amount_); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(ISwapFacility.PermissionedExtension.selector, address(mYieldToOne))); + swapFacility.swap(address(mYieldToOne), address(mYieldFee), amount_, alice); + + // Revoking the swapper right re-closes the swap-out M path. + vm.prank(admin); + swapFacility.setPermissionedMSwapper(address(mYieldToOne), alice, false); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(ISwapFacility.NotApprovedPermissionedSwapper.selector, address(mYieldToOne), alice) + ); + swapFacility.swapOutM(address(mYieldToOne), amount_, alice); + + // Lifting the permissioned flag returns the extension to the open M_SWAPPER_ROLE regime. + vm.prank(admin); + swapFacility.setPermissionedExtension(address(mYieldToOne), false); + + vm.prank(alice); + swapFacility.swapOutM(address(mYieldToOne), amount_, alice); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mToken.balanceOf(alice), amount_); + } + + /* ============ helpers ============ */ + + /// @dev Wraps `amount` $M into `extension` for `holder` through the real SwapFacility swap-in path. + function _wrapInto(IERC20 extension, address holder, uint256 amount) internal { + mToken.setBalanceOf(holder, mToken.balanceOf(holder) + amount); + + vm.prank(holder); + mToken.approve(address(swapFacility), amount); + + vm.prank(holder); + swapFacility.swapInM(address(extension), amount, holder); + } + + function _compressed(Vm.Wallet memory wallet) internal pure returns (bytes memory) { + return abi.encodePacked(wallet.publicKeyY % 2 == 0 ? bytes1(0x02) : bytes1(0x03), bytes32(wallet.publicKeyX)); + } + + function _extractPayload(bytes32 topic, address from, address to) internal returns (bytes memory payload) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i; i < logs.length; ++i) { + if ( + logs[i].topics[0] == topic && + logs[i].topics[1] == bytes32(uint256(uint160(from))) && + logs[i].topics[2] == bytes32(uint256(uint160(to))) + ) { + return abi.decode(logs[i].data, (bytes)); + } + } + revert("payload log not found"); + } +} diff --git a/test/integration/seismic/MYieldToOneSeismic.t.sol b/test/integration/seismic/MYieldToOneSeismic.t.sol new file mode 100644 index 00000000..b671337c --- /dev/null +++ b/test/integration/seismic/MYieldToOneSeismic.t.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +import { Vm } from "../../../lib/forge-std/src/Vm.sol"; +import { console } from "../../../lib/forge-std/src/console.sol"; + +import { Upgrades } from "../../../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; + +import { MYieldToOne } from "../../../src/projects/yieldToOne/MYieldToOne.sol"; +import { IMYieldToOne } from "../../../src/projects/yieldToOne/interfaces/IMYieldToOne.sol"; + +import { MYieldToOneHarness } from "../../harness/MYieldToOneHarness.sol"; + +import { BaseUnitTest } from "../../utils/BaseUnitTest.sol"; + +/// @dev Runs against the REAL Seismic precompiles (0x65 ECDH+KDF, 0x66/0x67 AES-GCM, 0x68 HKDF) — no mocks. +/// Requires sforge's seismic EVM (mercury); see README.md in this directory. +contract MYieldToOneSeismicIntegrationTests is BaseUnitTest { + // Pinned precompile vectors, cross-validated off-chain (RFC 5869 HKDF-SHA256, AES-256-GCM, + // libsecp256k1 ECDH) and reproduced by script/decrypt-transfer-event.py --self-test. + bytes32 internal constant ECDH_VECTOR_OUT = 0xa59676edf7d8f47a0cc8ac42e29566d4a1763eeeba8794d6a196029a1477f147; + bytes32 internal constant HKDF_VECTOR_OUT = 0xeb3cb17dcdc55d1119c98cac1e12bb13a95f81b50a0784750bdcf92787c4985e; + bytes internal constant AES_VECTOR_OUT = + hex"bae51be0c992f7c0c34d03a9431aebdc7732c399729054925a094d8f373d65c18c9eec49034da029df615639016e16c8"; + + // Generator point G compressed (privkey 1's public key). + bytes internal constant COMPRESSED_G = hex"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; + // 33 bytes, valid 0x02 prefix, x = 5 is not on secp256k1 (5^3 + 7 is a non-residue). + bytes internal constant OFF_CURVE_KEY = hex"020000000000000000000000000000000000000000000000000000000000000005"; + + bytes32 internal constant TRANSFER_BYTES_TOPIC = keccak256("Transfer(address,address,bytes)"); + bytes32 internal constant APPROVAL_BYTES_TOPIC = keccak256("Approval(address,address,bytes)"); + + MYieldToOneHarness public mYieldToOne; + + Vm.Wallet public contractWallet; + Vm.Wallet public recipientWallet; + + address public recipient; + + function setUp() public override { + super.setUp(); + + mYieldToOne = MYieldToOneHarness( + Upgrades.deployTransparentProxy( + "MYieldToOneHarness.sol:MYieldToOneHarness", + admin, + abi.encodeWithSelector( + MYieldToOne.initialize.selector, + "Seismic Dollar", + "USDS", + yieldRecipient, + admin, + freezeManager, + yieldRecipientManager, + pauser + ), + mExtensionDeployOptions + ) + ); + + registrar.setEarner(address(mYieldToOne), true); + + contractWallet = vm.createWallet("seismic contract key"); + recipientWallet = vm.createWallet("seismic recipient key"); + recipient = recipientWallet.addr; + + vm.prank(admin); + mYieldToOne.setContractKey(sbytes32(bytes32(contractWallet.privateKey)), _compressed(contractWallet)); + + vm.prank(recipient); + mYieldToOne.registerPublicKey(_compressed(recipientWallet)); + } + + /* ============ precompile semantics (pinned vectors) ============ */ + + function test_precompile_ecdh_pinnedVector() external view { + // 0x65 = ECDH + HKDF-SHA256(salt=none, info="aes-gcm key"): already a symmetric key, not a raw point. + assertEq(_ecdh(bytes32(uint256(1)), COMPRESSED_G), ECDH_VECTOR_OUT); + } + + function test_precompile_ecdh_symmetry() external view { + assertEq( + _ecdh(bytes32(contractWallet.privateKey), _compressed(recipientWallet)), + _ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet)) + ); + } + + function test_precompile_ecdh_offCurvePoint_fails() external view { + (bool success, ) = address(0x65).staticcall(abi.encodePacked(bytes32(uint256(1)), OFF_CURVE_KEY)); + assertFalse(success); + } + + function test_precompile_hkdf_pinnedVector() external view { + // 0x68 = HKDF-SHA256(salt=none, info="seismic_hkdf_105"), 32-byte output. + assertEq(_hkdf(bytes32(uint256(0xdeadbeef))), HKDF_VECTOR_OUT); + } + + function test_precompile_aesGcm_pinnedVector_andInverse() external view { + bytes32 key = bytes32(uint256(0x42)); + bytes12 nonce = bytes12(uint96(7)); + bytes memory plaintext = abi.encode(uint256(123456)); + + // 0x66 = AES-256-GCM encrypt; output = ciphertext || 16-byte tag. + bytes memory ciphertext = _aesGcmEncrypt(key, nonce, plaintext); + assertEq(ciphertext, AES_VECTOR_OUT); + + // 0x67 = AES-256-GCM decrypt (inverse, tag-checked). + assertEq(_aesGcmDecrypt(key, nonce, ciphertext), plaintext); + } + + /* ============ shielded transfer: real-key decryption round-trip ============ */ + + function test_shieldedTransfer_realKeys_recipientDecrypts() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setTotalSupply(amount); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.transfer(recipient, suint256(amount)); + + bytes memory ciphertext = _extractPayload(TRANSFER_BYTES_TOPIC, alice, recipient); + + // Contract-side determinism: re-derive the exact emit pipeline. + bytes32 contractSideKey = _hkdf(_ecdh(bytes32(contractWallet.privateKey), _compressed(recipientWallet))); + bytes12 nonce = bytes12(keccak256(abi.encode(alice, recipient, uint256(1)))); + assertEq(ciphertext, _aesGcmEncrypt(contractSideKey, nonce, abi.encode(amount))); + + // Recipient-side decryption: only the recipient's privkey + the public contract key. + bytes32 recipientSideKey = _hkdf(_ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet))); + assertEq(recipientSideKey, contractSideKey); + assertEq(abi.decode(_aesGcmDecrypt(recipientSideKey, nonce, ciphertext), (uint256)), amount); + + _logDecryptorVector(ciphertext); + } + + function test_shieldedTransferFrom_realKeys_recipientDecrypts() external { + uint256 amount = 250e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setTotalSupply(amount); + mYieldToOne.setShieldedAllowance(alice, carol, amount); + + vm.recordLogs(); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, recipient, suint256(amount)); + + bytes memory ciphertext = _extractPayload(TRANSFER_BYTES_TOPIC, alice, recipient); + + // Nonce binds (sender, recipient), not the spender. + bytes12 nonce = bytes12(keccak256(abi.encode(alice, recipient, uint256(1)))); + bytes32 key = _hkdf(_ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet))); + assertEq(abi.decode(_aesGcmDecrypt(key, nonce, ciphertext), (uint256)), amount); + } + + function test_shieldedApprove_realKeys_spenderDecrypts() external { + uint256 amount = 777e6; + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.approve(recipient, suint256(amount)); + + bytes memory ciphertext = _extractPayload(APPROVAL_BYTES_TOPIC, alice, recipient); + + bytes12 nonce = bytes12(keccak256(abi.encode(alice, recipient, uint256(1)))); + bytes32 key = _hkdf(_ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet))); + assertEq(abi.decode(_aesGcmDecrypt(key, nonce, ciphertext), (uint256)), amount); + } + + function test_shieldedTransfer_sequentialNonces_uniqueCiphertexts() external { + uint256 amount = 100e6; + mYieldToOne.setBalanceOf(alice, 2 * amount); + mYieldToOne.setTotalSupply(2 * amount); + + bytes32 key = _hkdf(_ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet))); + + vm.recordLogs(); + vm.prank(alice); + mYieldToOne.transfer(recipient, suint256(amount)); + bytes memory ciphertext1 = _extractPayload(TRANSFER_BYTES_TOPIC, alice, recipient); + + vm.recordLogs(); + vm.prank(alice); + mYieldToOne.transfer(recipient, suint256(amount)); + bytes memory ciphertext2 = _extractPayload(TRANSFER_BYTES_TOPIC, alice, recipient); + + assertNotEq(keccak256(ciphertext1), keccak256(ciphertext2)); + + bytes12 nonce1 = bytes12(keccak256(abi.encode(alice, recipient, uint256(1)))); + bytes12 nonce2 = bytes12(keccak256(abi.encode(alice, recipient, uint256(2)))); + assertEq(abi.decode(_aesGcmDecrypt(key, nonce1, ciphertext1), (uint256)), amount); + assertEq(abi.decode(_aesGcmDecrypt(key, nonce2, ciphertext2), (uint256)), amount); + } + + /* ============ off-curve registered key ============ */ + + function test_shieldedTransfer_offCurveRegisteredKey_revertsPrecompileFailed() external { + uint256 amount = 10e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setTotalSupply(amount); + + // Passes the contract's shape check (33 bytes, 0x02 prefix) but is not a curve point. + vm.prank(bob); + mYieldToOne.registerPublicKey(OFF_CURVE_KEY); + + // Inbound transfers to that account revert deterministically — no garbage ciphertext. + vm.expectRevert(abi.encodeWithSelector(IMYieldToOne.PrecompileFailed.selector, address(0x65))); + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + // Self-inflicted only: re-registering a valid key restores transfers. + vm.prank(bob); + mYieldToOne.registerPublicKey(_compressed(recipientWallet)); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + assertEq(mYieldToOne.getBalanceOf(bob), amount); + } + + /* ============ wrap -> shielded transfer -> unwrap (SwapFacility E2E) ============ */ + + function test_wrap_shieldedTransfer_unwrap_e2e() external { + uint256 amount = 5_000e6; + mToken.setBalanceOf(alice, amount); + + vm.prank(alice); + mToken.approve(address(swapFacility), amount); + + vm.prank(alice); + swapFacility.swapInM(address(mYieldToOne), amount, alice); + + assertEq(mYieldToOne.getBalanceOf(alice), amount); + assertEq(mYieldToOne.totalSupply(), amount); + + vm.recordLogs(); + vm.prank(alice); + mYieldToOne.transfer(recipient, suint256(amount)); + + bytes memory ciphertext = _extractPayload(TRANSFER_BYTES_TOPIC, alice, recipient); + bytes12 nonce = bytes12(keccak256(abi.encode(alice, recipient, uint256(1)))); + bytes32 key = _hkdf(_ecdh(bytes32(recipientWallet.privateKey), _compressed(contractWallet))); + assertEq(abi.decode(_aesGcmDecrypt(key, nonce, ciphertext), (uint256)), amount); + + vm.prank(admin); + swapFacility.grantRole(M_SWAPPER_ROLE, recipient); + + vm.prank(recipient); + mYieldToOne.approve(address(swapFacility), amount); + + vm.prank(recipient); + swapFacility.swapOutM(address(mYieldToOne), amount, recipient); + + assertEq(mYieldToOne.getBalanceOf(recipient), 0); + assertEq(mYieldToOne.totalSupply(), 0); + assertEq(mToken.balanceOf(recipient), amount); + } + + /* ============ helpers ============ */ + + function _ecdh(bytes32 privKey, bytes memory peerPubKey) internal view returns (bytes32) { + (bool success, bytes memory result) = address(0x65).staticcall(abi.encodePacked(privKey, peerPubKey)); + require(success, "ecdh precompile failed"); + return abi.decode(result, (bytes32)); + } + + function _hkdf(bytes32 ikm) internal view returns (bytes32) { + (bool success, bytes memory result) = address(0x68).staticcall(abi.encodePacked(ikm)); + require(success, "hkdf precompile failed"); + return abi.decode(result, (bytes32)); + } + + function _aesGcmEncrypt(bytes32 key, bytes12 nonce, bytes memory plaintext) internal view returns (bytes memory) { + (bool success, bytes memory ciphertext) = address(0x66).staticcall(abi.encodePacked(key, nonce, plaintext)); + require(success, "aes-gcm encrypt precompile failed"); + return ciphertext; + } + + function _aesGcmDecrypt(bytes32 key, bytes12 nonce, bytes memory ciphertext) internal view returns (bytes memory) { + (bool success, bytes memory plaintext) = address(0x67).staticcall(abi.encodePacked(key, nonce, ciphertext)); + require(success, "aes-gcm decrypt precompile failed"); + return plaintext; + } + + function _compressed(Vm.Wallet memory wallet) internal pure returns (bytes memory) { + return abi.encodePacked(wallet.publicKeyY % 2 == 0 ? bytes1(0x02) : bytes1(0x03), bytes32(wallet.publicKeyX)); + } + + function _extractPayload(bytes32 topic, address from, address to) internal returns (bytes memory payload) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i; i < logs.length; ++i) { + if ( + logs[i].topics[0] == topic && + logs[i].topics[1] == bytes32(uint256(uint160(from))) && + logs[i].topics[2] == bytes32(uint256(uint160(to))) + ) { + return abi.decode(logs[i].data, (bytes)); + } + } + revert("payload log not found"); + } + + function _logDecryptorVector(bytes memory ciphertext) internal view { + console.log("script/decrypt-transfer-event.py test vector:"); + console.log(" --privkey (recipient)"); + console.logBytes32(bytes32(recipientWallet.privateKey)); + console.log(" --peer-pubkey (contract)"); + console.logBytes(_compressed(contractWallet)); + console.log(" --from"); + console.log(alice); + console.log(" --to"); + console.log(recipient); + console.log(" --nonce-counter 1"); + console.log(" --ciphertext"); + console.logBytes(ciphertext); + } +} diff --git a/test/integration/seismic/README.md b/test/integration/seismic/README.md new file mode 100644 index 00000000..0749fbb3 --- /dev/null +++ b/test/integration/seismic/README.md @@ -0,0 +1,82 @@ +# Seismic integration suite + +End-to-end evidence that the encrypted-event pipeline works against the **real** Seismic +crypto — no `vm.mockCall`. Three independent legs plus the off-chain decryptor +(`script/decrypt-transfer-event.py`). + +## 1. In-process suite — `MYieldToOneSeismic.t.sol` + +sforge's seismic EVM implements the precompiles natively (the unit suite mocks them for +determinism, not necessity), so this runs as a plain test suite: + +```bash +source scripts/seismic-env.sh +FOUNDRY_NO_MATCH_PATH='no_match_nothing/**' sforge test --match-path 'test/integration/seismic/*' +``` + +The `FOUNDRY_NO_MATCH_PATH` override is required (same idiom as `make integration`): +`[profile.seismic]` sets `no_match_path = "test/integration/**"` because the +mainnet-fork suites need `eth_getFlaggedStorageAt`; note `*` crosses `/` in foundry +globs, so the replacement glob must not touch `test/integration` at all. + +Covers: pinned precompile vectors (executable documentation of 0x65/0x66/0x67/0x68 +semantics), ECDH symmetry, real-key shielded `transfer`/`transferFrom`/`approve` with +ciphertext reproduction and in-EVM decryption (0x67), nonce-counter evolution, off-curve +registered-key behavior (deterministic `PrecompileFailed(0x65)` on inbound transfers, +recoverable by re-registering), and the SwapFacility wrap → shielded transfer → unwrap +round-trip. `test_shieldedTransfer_realKeys_recipientDecrypts` logs a complete test +vector for the off-chain decryptor (run with `-vv`). + +## 2. Local-node E2E — `run-sanvil-e2e.sh` + +Boots a throwaway sanvil, deploys the stack via `SanvilStack.s.sol`, and drives it over +RPC with scast — the same transaction shapes ops/users send on the live network: + +```bash +bash test/integration/seismic/run-sanvil-e2e.sh # SANVIL_PORT=… to override (default 8547) +``` + +Asserts: `setContractKey` lands as **TxSeismic 0x4A** with the private key absent from +on-chain calldata; signed-read gating (unsigned `balanceOf` reverts, a spoofed `--from` +is rejected by the node itself, a signed stranger read reverts, the signed holder read +returns the balance); shielded transfer via encrypted calldata; off-chain decryption of +the captured `Transfer(bytes)` payload recovering the exact amount; wrap/unwrap through +the real SwapFacility path. Self-contained and idempotent; exits non-zero on any failure. + +sanvil quirks worth knowing (discovered empirically): + +- Plain-tx **gas estimation runs as an unsigned call** (`msg.sender` zeroed), so + msg.sender-gated plain sends need `--gas-limit`. TxSeismic estimation is signed. +- `scast send --seismic --json` prefixes the receipt JSON with the ephemeral encryption + pubkey line. +- Revert data returned by a signed `seismic_call` comes back AES-GCM-encrypted + (4-byte selector ciphertext + 16-byte tag). + +## 3. Live-testnet checklist — `check-live-testnet.sh` + +Read-only status of the chain-5124 USDS deployment (sends nothing): + +```bash +bash test/integration/seismic/check-live-testnet.sh +``` + +`[PENDING]` lines are expected until the post-deploy chain has run (`make configure-extension-seismic-testnet`, then `make set-contract-key-seismic-testnet`). Once +the contract key is installed, a live shielded-transfer smoke (register → transfer → +decrypt) can reuse the exact scast/decryptor commands from `run-sanvil-e2e.sh` against +`https://testnet-1.seismictest.net/rpc`. + +## Pinned crypto semantics (chain ⇄ off-chain contract) + +Verified against seismic-revm/enclave sources and reproduced off-chain; the in-process +suite and `script/decrypt-transfer-event.py --self-test` both assert these vectors. + +| Precompile | Semantics | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `0x65` ECDH | in: 32-byte secp256k1 privkey ‖ 33-byte compressed pubkey. out: HKDF-SHA256(salt=∅, info=`"aes-gcm key"`) of the libsecp256k1 shared secret (= SHA-256 of the compressed shared point) — already a derived key, **not** the raw x-coordinate. Errors on off-curve points. | +| `0x68` HKDF | out: HKDF-SHA256(salt=∅, info=`"seismic_hkdf_105"`, L=32) of the input bytes. | +| `0x66` / `0x67` AES-GCM | in: 32-byte key ‖ 12-byte nonce ‖ payload. AES-256-GCM; ciphertext layout `ct ‖ 16-byte tag`; empty AAD. `0x67` is the tag-checked inverse. | + +Event key = `0x68(0x65(contractPriv, recipientPub))` — a double HKDF. +Event nonce = first 12 bytes of `keccak256(abi.encode(from, to, nonceCounter))`, +`nonceCounter` pre-incremented per encrypted emit (1-based, shared across Transfer/Approval). +Plaintext = `abi.encode(uint256 amount)`. diff --git a/test/integration/seismic/SanvilStack.s.sol b/test/integration/seismic/SanvilStack.s.sol new file mode 100644 index 00000000..94a6f3ce --- /dev/null +++ b/test/integration/seismic/SanvilStack.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +import { Script } from "../../../lib/forge-std/src/Script.sol"; +import { console } from "../../../lib/forge-std/src/console.sol"; + +import { TransparentUpgradeableProxy } from "../../../lib/common/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { MYieldToOneForcedTransfer } from "../../../src/projects/yieldToOne/MYieldToOneForcedTransfer.sol"; +import { SwapFacility } from "../../../src/swap/SwapFacility.sol"; + +import { MockM, MockRegistrar } from "../../utils/Mocks.sol"; + +/// @dev Deploys a self-contained MYieldToOneForcedTransfer stack on a local sanvil node. +/// Driven by run-sanvil-e2e.sh — NOT for any public network (MockM, deployer holds every role). +contract SanvilStack is Script { + function run() external { + address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); + + vm.startBroadcast(deployer); + + MockM mToken = new MockM(); + MockRegistrar registrar = new MockRegistrar(); + + SwapFacility swapFacility = SwapFacility( + address( + new TransparentUpgradeableProxy( + address(new SwapFacility(address(mToken), address(registrar))), + deployer, + abi.encodeWithSelector(SwapFacility.initialize.selector, deployer, deployer) + ) + ) + ); + + MYieldToOneForcedTransfer extension = MYieldToOneForcedTransfer( + address( + new TransparentUpgradeableProxy( + address(new MYieldToOneForcedTransfer(address(mToken), address(swapFacility))), + deployer, + abi.encodeWithSelector( + MYieldToOneForcedTransfer.initialize.selector, + "Seismic Dollar (sanvil)", + "USDS", + deployer, // yieldRecipient + deployer, // admin + deployer, // freezeManager + deployer, // yieldRecipientManager + deployer, // pauser + deployer // forcedTransferManager + ) + ) + ) + ); + + registrar.setEarner(address(extension), true); + swapFacility.grantRole(swapFacility.M_SWAPPER_ROLE(), deployer); + + vm.stopBroadcast(); + + console.log("M_TOKEN=%s", address(mToken)); + console.log("SWAP_FACILITY=%s", address(swapFacility)); + console.log("EXTENSION=%s", address(extension)); + } +} diff --git a/test/integration/seismic/check-live-testnet.sh b/test/integration/seismic/check-live-testnet.sh new file mode 100755 index 00000000..d14ce807 --- /dev/null +++ b/test/integration/seismic/check-live-testnet.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Read-only status checklist for the live Seismic-testnet USDS deployment (chain 5124). +# Sends NO transactions. PENDING lines are expected until the post-deploy chain +# (make configure-extension-seismic-testnet, then make set-contract-key-seismic-testnet) has been run. +# +# Usage: bash test/integration/seismic/check-live-testnet.sh (SEISMIC_TESTNET_RPC_URL to override) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +[ -f "$REPO_ROOT/scripts/seismic-env.sh" ] && source "$REPO_ROOT/scripts/seismic-env.sh" > /dev/null + +CAST=$(command -v scast || command -v cast) || { echo "FAIL: need scast or cast on PATH"; exit 1; } + +RPC="${SEISMIC_TESTNET_RPC_URL:-https://testnet-1.seismictest.net/rpc}" +EXTENSION=0xb3b2f21f9a6a5d698D9178986Fa4148260B5d018 +SWAP_FACILITY=0xB6807116b3B1B321a390594e31ECD6e0076f6278 +PORTAL=0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd + +ok() { echo "[ok] $1"; } +pending() { echo "[PENDING] $1"; } +failed() { echo "[FAIL] $1"; FAILURES=$((FAILURES + 1)); } +FAILURES=0 + +CHAIN_ID=$("$CAST" chain-id --rpc-url "$RPC" 2> /dev/null) || { echo "FAIL: RPC unreachable: $RPC"; exit 1; } +[ "$CHAIN_ID" = "5124" ] && ok "RPC up, chain id 5124" || failed "unexpected chain id: $CHAIN_ID" + +CODE=$("$CAST" code $EXTENSION --rpc-url "$RPC") +[ "$CODE" != "0x" ] && ok "extension proxy has code ($EXTENSION)" || failed "no code at extension proxy" + +NAME=$("$CAST" call $EXTENSION "name()(string)" --rpc-url "$RPC") +SYMBOL=$("$CAST" call $EXTENSION "symbol()(string)" --rpc-url "$RPC") +[ "$NAME" = '"Seismic Dollar"' ] && ok "name/symbol: $NAME / $SYMBOL" || failed "unexpected name: $NAME" + +SUPPLY=$("$CAST" call $EXTENSION "totalSupply()(uint256)" --rpc-url "$RPC") +ok "totalSupply: $SUPPLY" + +PAUSED=$("$CAST" call $EXTENSION "paused()(bool)" --rpc-url "$RPC") +[ "$PAUSED" = "false" ] && ok "not paused" || failed "paused = $PAUSED" + +KEY=$("$CAST" call $EXTENSION "contractPublicKey()(bytes)" --rpc-url "$RPC") +if [ "$KEY" != "0x" ]; then + ok "contract key installed: $KEY" +else + pending "contract key NOT set (run: make set-contract-key-seismic-testnet) — shielded transfers to registered recipients revert until installed" +fi + +APPROVED=$("$CAST" call $SWAP_FACILITY "isApprovedExtension(address)(bool)" $EXTENSION --rpc-url "$RPC") +if [ "$APPROVED" = "true" ]; then + ok "extension approved on SwapFacility" +else + pending "extension NOT approved on SwapFacility (run: make configure-extension-seismic-testnet) — wrapping unavailable" +fi + +PORTAL_LISTED=$("$CAST" call $EXTENSION "isAllowlisted(address)(bool)" $PORTAL --rpc-url "$RPC") +if [ "$PORTAL_LISTED" = "true" ]; then + ok "Portal allowlisted" +else + pending "Portal NOT allowlisted (part of configure-extension)" +fi + +# Gate probe: an unsigned read of a third-party balance must revert (signed reads only). +if "$CAST" call $EXTENSION "balanceOf(address)(uint256)" 0x000000000000000000000000000000000000dEaD --rpc-url "$RPC" > /dev/null 2>&1; then + failed "balanceOf gate: unsigned third-party read DID NOT revert" +else + ok "balanceOf gate active (unsigned third-party read reverts)" +fi + +echo +if [ "$FAILURES" -gt 0 ]; then + echo "$FAILURES check(s) FAILED" + exit 1 +fi +echo "checklist complete (PENDING items await the post-deploy chain — the configure-extension + set-contract-key make targets)" diff --git a/test/integration/seismic/run-sanvil-e2e.sh b/test/integration/seismic/run-sanvil-e2e.sh new file mode 100755 index 00000000..b80eae88 --- /dev/null +++ b/test/integration/seismic/run-sanvil-e2e.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Sanvil E2E: deploy -> setContractKey (TxSeismic 0x4A) -> wrap -> shielded transfer -> +# off-chain decrypt -> unwrap, plus signed-read gating checks. Local node only; exits non-zero on failure. +# +# Usage: bash test/integration/seismic/run-sanvil-e2e.sh (SANVIL_PORT to override, default 8547) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$REPO_ROOT" +source scripts/seismic-env.sh > /dev/null + +for bin in sforge sanvil scast python3; do + command -v "$bin" > /dev/null || { echo "FAIL: $bin not on PATH (run: source scripts/seismic-env.sh && sfoundryup)"; exit 1; } +done + +PORT="${SANVIL_PORT:-8547}" +RPC="http://127.0.0.1:$PORT" +DECRYPTOR="$REPO_ROOT/script/decrypt-transfer-event.py" + +# sanvil dev accounts: 0 = deployer/admin/alice (every role), 1 = bob (recipient). +ALICE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +ALICE=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +BOB_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +BOB=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +# Throwaway contract keypair (local node only — NEVER reuse a fixed key on a public network). +CONTRACT_KEY=0x1111111111111111111111111111111111111111111111111111111111111111 + +WRAP_AMOUNT=5000000000 +TRANSFER_AMOUNT=1234567890 + +PASS=0 +step() { PASS=$((PASS + 1)); echo "[$PASS] $1"; } +fail() { echo "FAIL: $1"; exit 1; } + +# NOTE: `scast send --seismic --json` prefixes the receipt JSON with the ephemeral encryption pubkey. +json_get() { python3 -c " +import json, sys +raw = sys.stdin.read() +print(json.loads(raw[raw.index('{'):])$1)"; } + +# ---- boot a fresh sanvil ---- +pkill -f "sanvil --port $PORT" 2> /dev/null || true +sleep 0.5 +sanvil --port "$PORT" --silent > /tmp/sanvil-e2e.log 2>&1 & +SANVIL_PID=$! +trap 'kill $SANVIL_PID 2>/dev/null || true' EXIT + +for _ in $(seq 1 30); do + curl -s -X POST "$RPC" -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' | grep -q result && break + sleep 0.5 +done +curl -s -X POST "$RPC" -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' | grep -q result || fail "sanvil did not start (see /tmp/sanvil-e2e.log)" +step "sanvil up on $RPC" + +# ---- deploy the stack ---- +DEPLOY_OUT=$(PRIVATE_KEY=$ALICE_KEY sforge script test/integration/seismic/SanvilStack.s.sol \ + --rpc-url "$RPC" --broadcast --private-key $ALICE_KEY 2>&1) || { echo "$DEPLOY_OUT" | tail -20; fail "deploy"; } +M_TOKEN=$(echo "$DEPLOY_OUT" | sed -n 's/.*M_TOKEN=\(0x[0-9a-fA-F]*\).*/\1/p' | head -1) +SWAP_FACILITY=$(echo "$DEPLOY_OUT" | sed -n 's/.*SWAP_FACILITY=\(0x[0-9a-fA-F]*\).*/\1/p' | head -1) +EXTENSION=$(echo "$DEPLOY_OUT" | sed -n 's/.*EXTENSION=\(0x[0-9a-fA-F]*\).*/\1/p' | head -1) +[ -n "$EXTENSION" ] || fail "could not parse deployed addresses" +step "stack deployed: extension=$EXTENSION swapFacility=$SWAP_FACILITY mToken=$M_TOKEN" + +# ---- derive the contract's compressed pubkey ---- +CONTRACT_PUB_UNCOMPRESSED=$(scast wallet public-key --raw-private-key $CONTRACT_KEY) +CONTRACT_PUB=$(python3 -c " +pub = '${CONTRACT_PUB_UNCOMPRESSED#0x}' +x, y = pub[:64], pub[64:] +print(('0x02' if int(y, 16) % 2 == 0 else '0x03') + x)") +step "contract keypair derived: pubkey=$CONTRACT_PUB" + +# ---- setContractKey via TxSeismic 0x4A (shielded calldata) ---- +RECEIPT=$(scast send "$EXTENSION" "setContractKey(sbytes32,bytes)" $CONTRACT_KEY "$CONTRACT_PUB" \ + --private-key $ALICE_KEY --rpc-url "$RPC" --seismic --json) +[ "$(echo "$RECEIPT" | json_get "['status']")" = "0x1" ] || fail "setContractKey reverted" +[ "$(echo "$RECEIPT" | json_get "['type']")" = "0x4a" ] || fail "setContractKey was not TxSeismic (type 0x4A)" +TXH=$(echo "$RECEIPT" | json_get "['transactionHash']") +TX_INPUT=$(curl -s -X POST "$RPC" -H 'Content-Type: application/json' \ + -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getTransactionByHash\",\"params\":[\"$TXH\"]}" | json_get "['result']['input']") +echo "$TX_INPUT" | grep -q "${CONTRACT_KEY#0x}" && fail "private key visible in on-chain calldata" +[ "$(scast call "$EXTENSION" 'contractPublicKey()(bytes)' --rpc-url "$RPC")" = "$CONTRACT_PUB" ] || fail "contractPublicKey mismatch" +step "setContractKey: type 0x4A, calldata shielded, key installed" + +# ---- bob registers his real pubkey (plain tx — pubkeys are public) ---- +BOB_PUB_UNCOMPRESSED=$(scast wallet public-key --raw-private-key $BOB_KEY) +BOB_PUB=$(python3 -c " +pub = '${BOB_PUB_UNCOMPRESSED#0x}' +x, y = pub[:64], pub[64:] +print(('0x02' if int(y, 16) % 2 == 0 else '0x03') + x)") +scast send "$EXTENSION" "registerPublicKey(bytes)" "$BOB_PUB" --private-key $BOB_KEY --rpc-url "$RPC" > /dev/null +step "bob registered pubkey $BOB_PUB" + +# ---- wrap M -> extension (real SwapFacility path) ---- +# NOTE: plain-tx gas estimation runs as an unsigned call (msg.sender zeroed) on sanvil, so any +# msg.sender-gated call needs an explicit --gas-limit. +scast send "$M_TOKEN" "setBalanceOf(address,uint256)" $ALICE $WRAP_AMOUNT --private-key $ALICE_KEY --rpc-url "$RPC" > /dev/null +scast send "$M_TOKEN" "approve(address,uint256)" "$SWAP_FACILITY" $WRAP_AMOUNT --private-key $ALICE_KEY --rpc-url "$RPC" > /dev/null +scast send "$SWAP_FACILITY" "swapInM(address,uint256,address)" "$EXTENSION" $WRAP_AMOUNT $ALICE \ + --private-key $ALICE_KEY --rpc-url "$RPC" --gas-limit 1500000 > /dev/null +BALANCE=$(scast call "$EXTENSION" "balanceOf(address)" $ALICE --private-key $ALICE_KEY --rpc-url "$RPC" --seismic) +[ $((BALANCE)) -eq $WRAP_AMOUNT ] || fail "wrap: expected balance $WRAP_AMOUNT, got $((BALANCE))" +step "wrapped $WRAP_AMOUNT via swapInM" + +# ---- signed-read gating ---- +scast call "$EXTENSION" "balanceOf(address)" $ALICE --rpc-url "$RPC" > /dev/null 2>&1 \ + && fail "unsigned balanceOf did not revert" || true +scast call "$EXTENSION" "balanceOf(address)" $ALICE --from $BOB --rpc-url "$RPC" > /dev/null 2>&1 \ + && fail "unsigned balanceOf with spoofed from did not revert" || true +scast call "$EXTENSION" "balanceOf(address)" $ALICE --private-key $BOB_KEY --rpc-url "$RPC" --seismic > /dev/null 2>&1 \ + && fail "signed balanceOf from stranger did not revert" || true +step "balanceOf gate: unsigned reverts, spoofed-from rejected by node, signed stranger reverts, signed holder reads" + +# ---- shielded transfer alice -> bob (TxSeismic; amount inside encrypted calldata) ---- +RECEIPT=$(scast send "$EXTENSION" "transfer(address,suint256)" $BOB $TRANSFER_AMOUNT \ + --private-key $ALICE_KEY --rpc-url "$RPC" --seismic --json) +[ "$(echo "$RECEIPT" | json_get "['status']")" = "0x1" ] || fail "shielded transfer reverted" +[ "$(echo "$RECEIPT" | json_get "['type']")" = "0x4a" ] || fail "transfer was not TxSeismic" +TRANSFER_BYTES_TOPIC=$(scast keccak "Transfer(address,address,bytes)") +CIPHERTEXT=$(echo "$RECEIPT" | python3 -c " +import json, sys +raw = sys.stdin.read() +receipt = json.loads(raw[raw.index('{'):]) +for log in receipt['logs']: + if log['topics'][0] == '$TRANSFER_BYTES_TOPIC': + data = bytes.fromhex(log['data'][2:]) + length = int.from_bytes(data[32:64], 'big') + print('0x' + data[64:64 + length].hex()) + break +else: + sys.exit('Transfer(bytes) log not found')") +step "captured encrypted Transfer payload: $CIPHERTEXT" + +# ---- off-chain decryption (recipient privkey + contract pubkey only) ---- +DECRYPTED=$(python3 "$DECRYPTOR" \ + --privkey $BOB_KEY --peer-pubkey "$CONTRACT_PUB" \ + --from $ALICE --to $BOB --nonce-counter 1 --ciphertext "$CIPHERTEXT") +echo "$DECRYPTED" | grep -q "amount: $TRANSFER_AMOUNT" || fail "decryptor recovered wrong amount: $DECRYPTED" +step "off-chain decrypt recovered exact amount $TRANSFER_AMOUNT" + +# ---- unwrap (bob -> M) ---- +scast send "$SWAP_FACILITY" "grantRole(bytes32,address)" "$(scast keccak 'M_SWAPPER_ROLE')" $BOB \ + --private-key $ALICE_KEY --rpc-url "$RPC" --gas-limit 500000 > /dev/null +scast send "$EXTENSION" "approve(address,uint256)" "$SWAP_FACILITY" $TRANSFER_AMOUNT \ + --private-key $BOB_KEY --rpc-url "$RPC" --gas-limit 500000 > /dev/null +scast send "$SWAP_FACILITY" "swapOutM(address,uint256,address)" "$EXTENSION" $TRANSFER_AMOUNT $BOB \ + --private-key $BOB_KEY --rpc-url "$RPC" --gas-limit 1500000 > /dev/null +M_BALANCE=$(scast call "$M_TOKEN" "balanceOf(address)(uint256)" $BOB --rpc-url "$RPC") +[ "${M_BALANCE%% *}" = "$TRANSFER_AMOUNT" ] || fail "unwrap: expected $TRANSFER_AMOUNT M, got $M_BALANCE" +BOB_EXT_BALANCE=$(scast call "$EXTENSION" "balanceOf(address)" $BOB --private-key $BOB_KEY --rpc-url "$RPC" --seismic) +[ $((BOB_EXT_BALANCE)) -eq 0 ] || fail "unwrap: bob extension balance not zero" +step "unwrapped $TRANSFER_AMOUNT back to M" + +echo +echo "ALL E2E CHECKS PASSED ($PASS steps)" diff --git a/test/unit/projects/JMIExtension.t.sol b/test/unit/projects/JMIExtension.t.sol index e98c0ff8..dbdcec29 100644 --- a/test/unit/projects/JMIExtension.t.sol +++ b/test/unit/projects/JMIExtension.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.26; +import { Vm } from "../../../lib/forge-std/src/Vm.sol"; + import { IERC20 } from "../../../lib/common/src/interfaces/IERC20.sol"; import { IERC20Extended } from "../../../lib/common/src/interfaces/IERC20Extended.sol"; @@ -386,7 +388,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.wrap(address(mockUSDC), alice, amount); - assertEq(jmi.balanceOf(alice), amount); + assertEq(jmi.getBalanceOf(alice), amount); assertEq(jmi.assetBalanceOf(address(mockUSDC)), amount); assertEq(jmi.totalAssets(), amount); assertEq(jmi.totalSupply(), amount); @@ -395,6 +397,45 @@ contract JMIExtensionUnitTests is BaseUnitTest { assertEq(mockUSDC.balanceOf(address(jmi)), amount); } + function test_wrap_assetDeposit_emitsPlaintextOnly() public { + uint256 amount = 1_000e6; + + _installContractKey(); + + vm.prank(alice); + jmi.registerPublicKey(_validPubKey(0xBB)); + + mockUSDC.mint(address(swapFacility), amount); + + vm.recordLogs(); + + vm.prank(address(swapFacility)); + jmi.wrap(address(mockUSDC), alice, amount); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Transfer(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Transfer(address,address,uint256)"); + + bool foundBytes; + bool foundPlaintext; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(jmi)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + } + } + + assertTrue(foundPlaintext, "missing plaintext Transfer(uint256) on asset deposit"); + assertFalse(foundBytes, "bytes-overload Transfer emitted on asset deposit"); + + assertEq(jmi.getEncryptedEventNonce(), 0); + assertEq(jmi.getBalanceOf(alice), amount); + } + function test_wrap_diffDecimals() public { uint256 mockDAIAmount = 1e18; uint256 mockAsset4DecimalsAmount = 1e4; @@ -417,7 +458,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.wrap(address(mockDAI), alice, mockDAIAmount); - assertEq(jmi.balanceOf(alice), extensionAmount); + assertEq(jmi.getBalanceOf(alice), extensionAmount); assertEq(jmi.assetBalanceOf(address(mockDAI)), mockDAIAmount); assertEq(jmi.totalAssets(), extensionAmount); assertEq(jmi.totalSupply(), extensionAmount); @@ -441,7 +482,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.wrap(address(mockAsset4Decimals), alice, mockAsset4DecimalsAmount); - assertEq(jmi.balanceOf(alice), extensionAmount * 2); + assertEq(jmi.getBalanceOf(alice), extensionAmount * 2); assertEq(jmi.assetBalanceOf(address(mockAsset4Decimals)), mockAsset4DecimalsAmount); assertEq(jmi.totalAssets(), extensionAmount * 2); assertEq(jmi.totalSupply(), extensionAmount * 2); @@ -509,7 +550,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { return; } - assertEq(jmi.balanceOf(alice), amount); + assertEq(jmi.getBalanceOf(alice), amount); assertEq(jmi.assetBalanceOf(address(mockUSDC)), amount); assertEq(jmi.totalAssets(), amount); assertEq(jmi.totalSupply(), amount); @@ -550,7 +591,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { return; } - assertEq(jmi.balanceOf(alice), extensionAmount); + assertEq(jmi.getBalanceOf(alice), extensionAmount); assertEq(jmi.assetBalanceOf(address(asset)), amount); assertEq(jmi.totalAssets(), extensionAmount); assertEq(jmi.totalSupply(), extensionAmount); @@ -604,7 +645,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { totalUnwrapAmount += unwrapAmount; assertEq(jmi.totalSupply(), totalSupply); - assertEq(jmi.balanceOf(address(swapFacility)), totalSupply); + assertEq(jmi.getBalanceOf(address(swapFacility)), totalSupply); assertEq(mToken.balanceOf(address(swapFacility)), totalUnwrapAmount); unwrapAmount = 499e6; @@ -619,7 +660,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { totalUnwrapAmount += unwrapAmount; assertEq(jmi.totalSupply(), totalSupply); - assertEq(jmi.balanceOf(address(swapFacility)), totalSupply); + assertEq(jmi.getBalanceOf(address(swapFacility)), totalSupply); assertEq(mToken.balanceOf(address(swapFacility)), totalUnwrapAmount); unwrapAmount = 500e6; @@ -634,7 +675,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { totalUnwrapAmount += unwrapAmount; assertEq(jmi.totalSupply(), totalSupply); - assertEq(jmi.balanceOf(address(swapFacility)), totalSupply); + assertEq(jmi.getBalanceOf(address(swapFacility)), totalSupply); assertEq(mToken.balanceOf(address(swapFacility)), totalUnwrapAmount); assertEq(mToken.balanceOf(address(jmi)), 0); @@ -676,13 +717,13 @@ contract JMIExtensionUnitTests is BaseUnitTest { } assertEq(jmi.totalSupply(), totalSupply - amount); - assertEq(jmi.balanceOf(address(swapFacility)), totalSupply - amount); + assertEq(jmi.getBalanceOf(address(swapFacility)), totalSupply - amount); assertEq(mToken.balanceOf(address(swapFacility)), amount); assertEq(mToken.balanceOf(address(jmi)), mSupply - amount); } - /* ============ transfer ============ */ + /* ============ transfer (shielded) ============ */ function test_transfer_enforcedPause() external { vm.prank(pauser); @@ -691,36 +732,110 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); vm.prank(alice); - jmi.transfer(bob, 1); + jmi.transfer(bob, suint256(1)); } - function test_transfer() external { + function test_shieldedTransfer() external { uint256 amount = 1_000e6; + _installContractKey(); + mockUSDC.mint(address(swapFacility), amount); assertEq(mockUSDC.balanceOf(address(swapFacility)), amount); vm.prank(address(swapFacility)); jmi.wrap(address(mockUSDC), alice, amount); - assertEq(jmi.balanceOf(alice), amount); - assertEq(jmi.balanceOf(bob), 0); + assertEq(jmi.getBalanceOf(alice), amount); + assertEq(jmi.getBalanceOf(bob), 0); assertEq(jmi.assetBalanceOf(address(mockUSDC)), amount); assertEq(jmi.totalAssets(), amount); assertEq(jmi.totalSupply(), amount); + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, bytes("")); + vm.prank(alice); - jmi.transfer(bob, amount); + jmi.transfer(bob, suint256(amount)); - assertEq(jmi.balanceOf(alice), 0); - assertEq(jmi.balanceOf(bob), amount); + assertEq(jmi.getBalanceOf(alice), 0); + assertEq(jmi.getBalanceOf(bob), amount); assertEq(jmi.assetBalanceOf(address(mockUSDC)), amount); assertEq(jmi.totalAssets(), amount); assertEq(jmi.totalSupply(), amount); } + function test_transfer_inheritedPathReverts() external { + vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); + + vm.prank(alice); + jmi.transfer(bob, 1_000e6); + } + + function test_transfer_inheritedPathRevertsWhenPaused() external { + vm.prank(pauser); + jmi.pause(); + + vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); + + vm.prank(alice); + jmi.transfer(bob, 1); + } + + /* ============ transferFrom (native) ============ */ + + function test_nativeTransferFrom_allowlistedCaller() public { + uint256 amount = 1_000e6; + uint256 allowanceAmount = 1_500e6; + jmi.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.prank(admin); + jmi.setAllowlisted(carol, true); + + vm.prank(alice); + jmi.approve(carol, suint256(allowanceAmount)); + + vm.expectEmit(); + emit IERC20.Transfer(alice, bob, amount); + + vm.prank(carol); + jmi.transferFrom(alice, bob, amount); + + assertEq(jmi.getBalanceOf(alice), 0); + assertEq(jmi.getBalanceOf(bob), amount); + + vm.prank(alice); + assertEq(jmi.allowance(alice, carol), allowanceAmount - amount); + } + + /* ============ balanceOf ============ */ + + function test_balanceOf_holderCanRead() public { + jmi.setBalanceOf(alice, 1_000e6); + + vm.prank(alice); + assertEq(jmi.balanceOf(alice), 1_000e6); + } + + function test_balanceOf_unauthorized() public { + jmi.setBalanceOf(alice, 1_000e6); + + vm.expectRevert(IMYieldToOne.Unauthorized.selector); + vm.prank(bob); + jmi.balanceOf(alice); + } + + function test_balanceOf_swapFacilityCanRead() public { + jmi.setBalanceOf(alice, 1_000e6); + + vm.prank(address(swapFacility)); + assertEq(jmi.balanceOf(alice), 1_000e6); + } + /* ============ replaceAssetWithM ============ */ function test_replaceAssetWithM_onlySwapFacility() external { @@ -847,7 +962,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.replaceAssetWithM(address(mockUSDC), alice, amount); - assertEq(jmi.balanceOf(alice), 0); + assertEq(jmi.getBalanceOf(alice), 0); assertEq(jmi.assetBalanceOf(address(mockUSDC)), 0); assertEq(jmi.totalAssets(), 0); assertEq(jmi.totalSupply(), amount); @@ -894,7 +1009,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.replaceAssetWithM(address(mockDAI), alice, extensionAmount); - assertEq(jmi.balanceOf(alice), 0); + assertEq(jmi.getBalanceOf(alice), 0); assertEq(jmi.totalAssets(), extensionAmount); assertEq(jmi.totalSupply(), totalAmount); @@ -920,7 +1035,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); jmi.replaceAssetWithM(address(mockAsset4Decimals), alice, extensionAmount); - assertEq(jmi.balanceOf(alice), 0); + assertEq(jmi.getBalanceOf(alice), 0); assertEq(jmi.assetBalanceOf(address(mockAsset4Decimals)), 0); assertEq(jmi.totalAssets(), 0); assertEq(jmi.totalSupply(), totalAmount); @@ -983,7 +1098,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { return; } - assertEq(jmi.balanceOf(alice), 0); + assertEq(jmi.getBalanceOf(alice), 0); assertEq(jmi.assetBalanceOf(address(mockUSDC)), usdcBacking - amount); assertEq(jmi.totalAssets(), usdcBacking - amount); assertEq(jmi.totalSupply(), extensionSupply); @@ -1078,7 +1193,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { return; } - assertEq(jmi.balanceOf(alice), 0); + assertEq(jmi.getBalanceOf(alice), 0); assertEq(jmi.assetBalanceOf(address(asset)), assetBacking - amount); assertEq(jmi.totalAssets(), extensionBacking - extensionAmount); assertEq(jmi.totalSupply(), extensionSupply); @@ -1137,7 +1252,7 @@ contract JMIExtensionUnitTests is BaseUnitTest { assertEq(jmi.totalSupply(), 3_000e6); assertEq(mToken.balanceOf(yieldRecipient), 0); - assertEq(jmi.balanceOf(yieldRecipient), yield); + assertEq(jmi.getBalanceOf(yieldRecipient), yield); } /* ============ _fromAssetToExtensionAmount ============ */ @@ -1192,6 +1307,22 @@ contract JMIExtensionUnitTests is BaseUnitTest { /* ============ Helper Functions ============ */ + /// @dev Returns a 33-byte compressed-secp256k1-shaped public key; contents are arbitrary. + function _validPubKey(bytes1 marker) internal pure returns (bytes memory) { + bytes memory key = new bytes(33); + key[0] = 0x02; // compressed-secp256k1 even-Y prefix + for (uint256 i = 1; i < 33; ++i) { + key[i] = marker; + } + return key; + } + + /// @dev Installs the contract keypair through the admin path. + function _installContractKey() internal { + vm.prank(admin); + jmi.setContractKey(sbytes32(bytes32(uint256(0xC0FFEE))), _validPubKey(0xAA)); + } + /// @dev Helper function to randomly select one of the 3 stablecoins based on a seed function _getRandomAsset(uint256 seed) internal view returns (MockERC20 asset, uint256 cap, uint8 decimals) { uint256 choice = seed % 3; diff --git a/test/unit/projects/yieldToOne/MYieldToOne.t.sol b/test/unit/projects/yieldToOne/MYieldToOne.t.sol index 65cc6218..eb60fa8c 100644 --- a/test/unit/projects/yieldToOne/MYieldToOne.t.sol +++ b/test/unit/projects/yieldToOne/MYieldToOne.t.sol @@ -178,7 +178,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_setAllowlisted_noUpdate() public { - // Setting an account to its current (default `false`) status is a no-op: no event. vm.recordLogs(); vm.prank(admin); @@ -194,7 +193,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertTrue(mYieldToOne.isAllowlisted(bob)); - // Re-setting the same status emits no second event and leaves state unchanged. vm.recordLogs(); vm.prank(admin); @@ -281,7 +279,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { /* ============ isAllowlisted ============ */ function test_isAllowlisted_swapFacilityNotAllowlisted() public view { - // swapFacility is permanently infra via the immutable, not via the allowlist mapping. assertFalse(mYieldToOne.isAllowlisted(address(swapFacility))); } @@ -310,8 +307,11 @@ contract MYieldToOneUnitTests is BaseUnitTest { function test_approve_writesShieldedStorage() public { uint256 amount = 1_000e6; - vm.expectEmit(); - emit IERC20.Approval(alice, bob, amount); + _installContractKey(); + + // bob has no registered key => empty-ciphertext fallback emit on the bytes overload. + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Approval(alice, bob, bytes("")); vm.prank(alice); mYieldToOne.approve(bob, suint256(amount)); @@ -320,7 +320,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_approve_inheritedPathReverts() public { - // The IERC20 `approve(address,uint256)` is overridden to revert at the entry point. vm.expectRevert(IMYieldToOne.UseShieldedApprove.selector); vm.prank(alice); @@ -328,8 +327,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_approve_permitReverts() public { - // Inherited EIP-2612 `permit` is overridden to revert directly at the entry point — - // both the v/r/s overload and the bytes-signature overload. vm.expectRevert(IMYieldToOne.UseShieldedApprove.selector); mYieldToOne.permit(alice, bob, 1_000e6, type(uint256).max, 0, bytes32(0), bytes32(0)); @@ -337,10 +334,9 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.permit(alice, bob, 1_000e6, type(uint256).max, ""); } - /* ============ approve (native, allowlist-gated) ============ */ + /* ============ approve (native) ============ */ function test_nativeApprove_nonInfraSpenderReverts() public { - // bob is not allowlisted and not the swapFacility → native path is closed. vm.expectRevert(IMYieldToOne.UseShieldedApprove.selector); vm.prank(alice); @@ -359,14 +355,12 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(alice); mYieldToOne.approve(bob, amount); - // Native path writes the SAME shielded slot as the shielded `approve(address,suint256)`. assertEq(mYieldToOne.getShieldedAllowance(alice, bob), amount); } function test_nativeApprove_swapFacilitySpender() public { uint256 amount = 1_000e6; - // swapFacility is permanently infra via the immutable — no allowlisting needed. vm.expectEmit(); emit IERC20.Approval(alice, address(swapFacility), amount); @@ -383,7 +377,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(freezeManager); mYieldToOne.freeze(alice); - // Freeze is still enforced on the native path (routes through `_beforeApprove`). vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); vm.prank(alice); @@ -403,14 +396,26 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.approve(bob, 1_000e6); } - /* ============ transferFrom (native, allowlist-gated) ============ */ + function test_nativeApprove_delistedSpenderReverts() public { + vm.prank(admin); + mYieldToOne.setAllowlisted(bob, true); + + vm.prank(admin); + mYieldToOne.setAllowlisted(bob, false); + + vm.expectRevert(IMYieldToOne.UseShieldedApprove.selector); + + vm.prank(alice); + mYieldToOne.approve(bob, 1_000e6); + } + + /* ============ transferFrom (native) ============ */ function test_nativeTransferFrom_nonInfraCallerReverts() public { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); mYieldToOne.setShieldedAllowance(alice, carol, amount); - // carol is not allowlisted and not the swapFacility → native path is closed. vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); vm.prank(carol); @@ -422,6 +427,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 allowanceAmount = 1_500e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -436,7 +443,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertEq(mYieldToOne.getBalanceOf(alice), 0); assertEq(mYieldToOne.getBalanceOf(bob), amount); - // Decrements the shared shielded allowance slot. assertEq(mYieldToOne.getShieldedAllowance(alice, carol), allowanceAmount - amount); } @@ -444,6 +450,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(address(swapFacility), suint256(amount)); @@ -462,6 +470,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -471,7 +481,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(carol); mYieldToOne.transferFrom(alice, bob, amount); - // Infinite allowance is preserved (matches the shielded path). assertEq(mYieldToOne.getShieldedAllowance(alice, carol), type(uint256).max); } @@ -479,13 +488,15 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); vm.prank(alice); mYieldToOne.approve(carol, suint256(amount - 1)); - // Allowance field zeroed in the revert payload — matches the shielded-balance precedent. + // Allowance is reported as 0 in the revert to avoid leaking the shielded value. vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAllowance.selector, carol, 0, amount)); vm.prank(carol); @@ -496,6 +507,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -505,7 +518,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(pauser); mYieldToOne.pause(); - // Pause is still enforced on the native path (routes through `_beforeTransfer`). vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); vm.prank(carol); @@ -516,6 +528,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -531,12 +545,63 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.transferFrom(alice, bob, amount); } + function test_nativeTransferFrom_frozenRecipient() public { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setShieldedAllowance(alice, carol, amount); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + vm.prank(freezeManager); + mYieldToOne.freeze(bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, bob)); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, amount); + } + + function test_nativeTransferFrom_frozenCaller() public { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setShieldedAllowance(alice, carol, amount); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + vm.prank(freezeManager); + mYieldToOne.freeze(carol); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, carol)); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, amount); + } + + function test_nativeTransferFrom_delistedCallerReverts() public { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + mYieldToOne.setShieldedAllowance(alice, carol, amount); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, false); + + vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, amount); + } + function test_nativeTransferFrom_shieldedApproveSpentByNativePath() public { - // Cross-consistency: a shielded `approve(suint256)` is spendable by a native - // `transferFrom(uint256)` from an allowlisted caller — proves the single shared slot. uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -551,11 +616,11 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_nativeApprove_spentByShieldedTransferFrom() public { - // Cross-consistency (reverse): a native `approve(uint256)` to an allowlisted spender is - // spendable by the shielded `transferFrom(suint256)` — proves the single shared slot. uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -592,7 +657,7 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertEq(mYieldToOne.getShieldedAllowance(alice, carol), 0); } - /* ============ balanceOf (gated read) ============ */ + /* ============ balanceOf ============ */ function test_balanceOf_holderCanRead() public { mYieldToOne.setBalanceOf(alice, 1_000e6); @@ -612,8 +677,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { function test_balanceOf_swapFacilityCanRead() public { mYieldToOne.setBalanceOf(alice, 1_000e6); - // SwapFacility is exempted so M0 infra can observe extension balances along its - // operational paths without forcing a Seismic signed read. vm.prank(address(swapFacility)); assertEq(mYieldToOne.balanceOf(alice), 1_000e6); } @@ -621,8 +684,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { function test_balanceOf_allowlistedInfraCanReadAnyHolder() public { mYieldToOne.setBalanceOf(alice, 1_000e6); - // An allowlisted infra contract (e.g. LimitOrderProtocol) reads an arbitrary holder's - // cleartext balance to drive its operational paths. vm.prank(admin); mYieldToOne.setAllowlisted(carol, true); @@ -630,6 +691,13 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertEq(mYieldToOne.balanceOf(alice), 1_000e6); } + function test_balanceOf_freezeManagerCanRead() public { + mYieldToOne.setBalanceOf(alice, 1_000e6); + + vm.prank(freezeManager); + assertEq(mYieldToOne.balanceOf(alice), 1_000e6); + } + function test_balanceOf_removingFromAllowlistReblocks() public { mYieldToOne.setBalanceOf(alice, 1_000e6); @@ -639,7 +707,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(carol); assertEq(mYieldToOne.balanceOf(alice), 1_000e6); - // Removing the address from the allowlist re-blocks its read. vm.prank(admin); mYieldToOne.setAllowlisted(carol, false); @@ -648,10 +715,11 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.balanceOf(alice); } - /* ============ allowance (gated read) ============ */ + /* ============ allowance ============ */ function test_allowance_unauthorized() public { - // alice approves bob; carol (third party) attempts to read → reverts. + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(bob, suint256(500e6)); @@ -661,6 +729,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_allowance_ownerCanRead() public { + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(bob, suint256(500e6)); @@ -669,6 +739,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_allowance_spenderCanRead() public { + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(bob, suint256(500e6)); @@ -772,6 +844,21 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.unwrap(alice, 1); } + function test_unwrap_insufficientBalance() external { + uint256 amount = 1_000e6; + + mYieldToOne.setBalanceOf(address(swapFacility), amount - 1); + mYieldToOne.setTotalSupply(amount - 1); + + // Balance is reported as 0 in the revert to avoid leaking the shielded value. + vm.expectRevert( + abi.encodeWithSelector(IMExtension.InsufficientBalance.selector, address(swapFacility), 0, amount) + ); + + vm.prank(address(swapFacility)); + mYieldToOne.unwrap(alice, amount); + } + function test_unwrap() external { uint256 amount = 1_000e6; @@ -819,6 +906,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + // Alice allows Carol to transfer tokens on her behalf (shielded approve). vm.prank(alice); mYieldToOne.approve(carol, suint256(amount)); @@ -875,8 +964,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); - // bob has no registered key => empty-ciphertext fallback emit (ciphertext assertions - // live in the encrypted-transfer tests below). + _installContractKey(); + vm.expectEmit(true, true, false, true); emit IMYieldToOne.Transfer(alice, bob, bytes("")); @@ -891,16 +980,39 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount - 1); - // Shielded comparison reverts with `balance = 0` (not the real balance) to avoid leak. + _installContractKey(); + vm.expectRevert(abi.encodeWithSelector(IMExtension.InsufficientBalance.selector, alice, 0, amount)); vm.prank(alice); mYieldToOne.transfer(bob, suint256(amount)); } + function test_transfer_selfTransfer() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, alice, bytes("")); + + vm.prank(alice); + mYieldToOne.transfer(alice, suint256(amount)); + + assertEq(mYieldToOne.getBalanceOf(alice), amount); + } + + function test_transfer_invalidRecipient() external { + mYieldToOne.setBalanceOf(alice, 1_000e6); + + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); + + vm.prank(alice); + mYieldToOne.transfer(address(0), suint256(1_000e6)); + } + function test_transfer_inheritedPathReverts() external { - // The IERC20 `transfer(address,uint256)` is overridden to revert at the entry point — - // no balance / freeze / pause state matters. vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); vm.prank(alice); @@ -918,6 +1030,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.setBalanceOf(alice, aliceBalance); mYieldToOne.setBalanceOf(bob, bobBalance); + _installContractKey(); + vm.prank(alice); mYieldToOne.transfer(bob, suint256(transferAmount)); @@ -932,11 +1046,11 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 allowanceAmount = 1_500e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(carol, suint256(allowanceAmount)); - // bob has not registered a public key, so the shielded entry point emits the - // bytes-variant Transfer overload with an empty ciphertext (empty-fallback branch). vm.expectEmit(true, true, false, true); emit IMYieldToOne.Transfer(alice, bob, bytes("")); @@ -952,13 +1066,14 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(carol, suint256(type(uint256).max)); vm.prank(carol); mYieldToOne.transferFrom(alice, bob, suint256(amount)); - // Infinite allowance is preserved (matches ERC20ExtendedUpgradeable.transferFrom semantics). assertEq(mYieldToOne.getShieldedAllowance(alice, carol), type(uint256).max); } @@ -966,10 +1081,11 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); + _installContractKey(); + vm.prank(alice); mYieldToOne.approve(carol, suint256(amount - 1)); - // Allowance field zeroed in the revert payload — matches the shielded-balance precedent. vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAllowance.selector, carol, 0, amount)); vm.prank(carol); @@ -980,16 +1096,76 @@ contract MYieldToOneUnitTests is BaseUnitTest { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); - // No prior approve — shielded allowance is zero. vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAllowance.selector, carol, 0, amount)); vm.prank(carol); mYieldToOne.transferFrom(alice, bob, suint256(amount)); } + function test_transferFrom_insufficientBalance() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount - 1); + mYieldToOne.setShieldedAllowance(alice, carol, amount); + + _installContractKey(); + + vm.expectRevert(abi.encodeWithSelector(IMExtension.InsufficientBalance.selector, alice, 0, amount)); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(amount)); + } + + function test_shieldedTransferFrom_spenderDelistedAfterApprove_stillSpends() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + vm.prank(alice); + mYieldToOne.approve(carol, amount); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, false); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(amount)); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), amount); + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), 0); + } + + function testFuzz_transferFrom( + uint256 supply, + uint256 aliceBalance, + uint256 transferAmount, + bool infiniteAllowance + ) external { + supply = bound(supply, 1, type(uint240).max); + aliceBalance = bound(aliceBalance, 1, supply); + transferAmount = bound(transferAmount, 1, aliceBalance); + uint256 bobBalance = supply - aliceBalance; + + if (bobBalance == 0) return; + + mYieldToOne.setBalanceOf(alice, aliceBalance); + mYieldToOne.setBalanceOf(bob, bobBalance); + mYieldToOne.setShieldedAllowance(alice, carol, infiniteAllowance ? type(uint256).max : transferAmount); + + _installContractKey(); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(transferAmount)); + + assertEq(mYieldToOne.getBalanceOf(alice), aliceBalance - transferAmount); + assertEq(mYieldToOne.getBalanceOf(bob), bobBalance + transferAmount); + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), infiniteAllowance ? type(uint256).max : 0); + } + function test_transferFrom_inheritedPathReverts() external { - // The IERC20 `transferFrom(address,address,uint256)` is overridden to revert at the - // entry point. vm.expectRevert(IMYieldToOne.UseShieldedTransfer.selector); vm.prank(carol); @@ -1105,10 +1281,9 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertEq(mYieldToOne.getBalanceOf(yieldRecipient), 500); } - /* ============ Encrypted Transfer Events — Helpers ============ */ + /* ============ Helpers ============ */ - /// @dev Canonical compressed-secp256k1 (33-byte) shape. Actual bytes are irrelevant here — - /// the precompiles (0x65/0x68/0x66) are mocked; Seismic validates their semantics on devnet. + /// @dev Returns a 33-byte compressed-secp256k1-shaped public key; contents are arbitrary. function _validPubKey(bytes1 marker) internal pure returns (bytes memory) { bytes memory key = new bytes(33); key[0] = 0x02; // compressed-secp256k1 even-Y prefix @@ -1118,21 +1293,14 @@ contract MYieldToOneUnitTests is BaseUnitTest { return key; } - /// @dev Installs deterministic mocks for the three Seismic precompiles so the - /// encrypted-emit pipeline can run end-to-end under plain `sforge`. The chosen - /// return values are arbitrary but distinct, so tests can assert the contract - /// forwards the precompile output as the event payload. + /// @dev Mocks the Seismic precompiles (0x65 ECDH, 0x68 HKDF, 0x66 AES-GCM) with distinct outputs. function _mockPrecompiles() internal { - // 0x65 ECDH → 32-byte shared secret. vm.mockCall(address(0x65), bytes(""), abi.encode(bytes32(uint256(1)))); - // 0x68 HKDF → 32-byte AES-GCM key. vm.mockCall(address(0x68), bytes(""), abi.encode(bytes32(uint256(2)))); - // 0x66 AES-GCM encrypt → opaque non-empty ciphertext. vm.mockCall(address(0x66), bytes(""), hex"deadbeefcafebabe"); } - /// @dev Installs a contract keypair through the admin path. The actual private-key - /// bytes are never observed externally (would require a Seismic signed read). + /// @dev Installs the contract keypair through the admin path. function _installContractKey() internal { vm.prank(admin); mYieldToOne.setContractKey(sbytes32(bytes32(uint256(0xC0FFEE))), _validPubKey(0xAA)); @@ -1153,7 +1321,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(admin); mYieldToOne.setContractKey(sbytes32(bytes32(uint256(0xC0FFEE))), _validPubKey(0xAA)); - // Second call must revert — rotation is intentionally not supported. vm.expectRevert(IMYieldToOne.ContractKeyAlreadySet.selector); vm.prank(admin); @@ -1178,9 +1345,28 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.setContractKey(sbytes32(bytes32(uint256(1))), tooLong); } + function test_setContractKey_zeroPrivateKey() public { + vm.expectRevert(IMYieldToOne.ZeroPrivateKey.selector); + + vm.prank(admin); + mYieldToOne.setContractKey(sbytes32(bytes32(0)), _validPubKey(0xAA)); + } + + function test_setContractKey_invalidPrefix() public { + bytes memory pubKey = _validPubKey(0xAA); + pubKey[0] = 0x04; + + vm.expectRevert(IMYieldToOne.InvalidPublicKeyPrefix.selector); + + vm.prank(admin); + mYieldToOne.setContractKey(sbytes32(bytes32(uint256(1))), pubKey); + } + function test_setContractKey_emitsContractKeySet() public { bytes memory pubKey = _validPubKey(0xAA); + assertEq(mYieldToOne.contractPublicKey(), bytes("")); + vm.expectEmit(); emit IMYieldToOne.ContractKeySet(pubKey); @@ -1195,6 +1381,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { function test_registerPublicKey_writesStorage() public { bytes memory pubKey = _validPubKey(0xBB); + assertEq(mYieldToOne.publicKeyOf(alice), bytes("")); + vm.prank(alice); mYieldToOne.registerPublicKey(pubKey); @@ -1210,8 +1398,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertEq(mYieldToOne.publicKeyOf(alice), firstKey); - // Re-registration overwrites — historical ciphertexts decrypt with the old key, - // future ciphertexts with the new one. vm.prank(alice); mYieldToOne.registerPublicKey(secondKey); @@ -1236,6 +1422,16 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.registerPublicKey(tooLong); } + function test_registerPublicKey_invalidPrefix() public { + bytes memory pubKey = _validPubKey(0xBB); + pubKey[0] = 0x04; + + vm.expectRevert(IMYieldToOne.InvalidPublicKeyPrefix.selector); + + vm.prank(alice); + mYieldToOne.registerPublicKey(pubKey); + } + function test_registerPublicKey_emitsPublicKeyRegistered() public { vm.expectEmit(); emit IMYieldToOne.PublicKeyRegistered(alice); @@ -1244,7 +1440,7 @@ contract MYieldToOneUnitTests is BaseUnitTest { mYieldToOne.registerPublicKey(_validPubKey(0xBB)); } - /* ============ Shielded Transfer — Encrypted Emit (registered recipient) ============ */ + /* ============ _encryptAmount ============ */ function test_shieldedTransfer_registeredRecipient_emitsBytesPayload() external { uint256 amount = 1_000e6; @@ -1264,10 +1460,8 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(alice); mYieldToOne.transfer(bob, suint256(amount)); - // Counter incremented exactly once for the single encrypted emit. assertEq(mYieldToOne.getEncryptedEventNonce(), 1); - // Locate the emitted bytes-variant Transfer log and assert shape. Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 bytesTopic = keccak256("Transfer(address,address,bytes)"); bytes32 plaintextTopic = keccak256("Transfer(address,address,uint256)"); @@ -1292,7 +1486,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertTrue(foundBytes, "missing Transfer(address,address,bytes) emit"); assertFalse(foundPlaintext, "plaintext Transfer(uint256) emitted on shielded path"); - // Balance updates still happen. assertEq(mYieldToOne.getBalanceOf(alice), 0); assertEq(mYieldToOne.getBalanceOf(bob), amount); } @@ -1344,55 +1537,65 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertFalse(foundPlaintext, "plaintext Transfer(uint256) emitted on shielded path"); } - /* ============ Shielded Transfer — Unregistered Recipient Fallback ============ */ - - function test_shieldedTransfer_unregisteredRecipient_emitsEmptyBytesAndSucceeds() external { + function test_shieldedTransfer_ciphertextMatchesPrecompileOutput() external { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); - // Contract key IS set — we want to isolate the unregistered-recipient branch from - // the no-contract-key branch (the fallback fires BEFORE the contract-key check, so - // both branches are independently testable). _installContractKey(); + _mockPrecompiles(); - // bob is intentionally NOT registered; precompiles intentionally NOT mocked — the - // fallback path must not call any of them. + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); vm.expectEmit(true, true, false, true); - emit IMYieldToOne.Transfer(alice, bob, bytes("")); + emit IMYieldToOne.Transfer(alice, bob, hex"deadbeefcafebabe"); vm.prank(alice); mYieldToOne.transfer(bob, suint256(amount)); + } - // Counter does NOT increment on the empty-bytes fallback (saves an SSTORE). - assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + function test_shieldedTransfer_forwardsContractAndRecipientKeysToEcdh() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); - // Balances still update. - assertEq(mYieldToOne.getBalanceOf(alice), 0); - assertEq(mYieldToOne.getBalanceOf(bob), amount); - } + _installContractKey(); + _mockPrecompiles(); - /* ============ Shielded Transfer — Contract Key Not Set ============ */ + bytes memory recipientKey = _validPubKey(0xBB); - function test_shieldedTransfer_contractKeyNotSet_reverts() external { + vm.prank(bob); + mYieldToOne.registerPublicKey(recipientKey); + + vm.expectCall(address(0x65), abi.encodePacked(bytes32(uint256(0xC0FFEE)), recipientKey)); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } + + function test_shieldedTransfer_reregisteredKeyForwardedToEcdh() external { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); - // Recipient IS registered but contract keypair is NOT installed → revert. + _installContractKey(); + _mockPrecompiles(); + + bytes memory firstKey = _validPubKey(0xBB); + bytes memory secondKey = _validPubKey(0xCC); + vm.prank(bob); - mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + mYieldToOne.registerPublicKey(firstKey); - vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + vm.prank(bob); + mYieldToOne.registerPublicKey(secondKey); + + vm.expectCall(address(0x65), abi.encodePacked(bytes32(uint256(0xC0FFEE)), firstKey), 0); + vm.expectCall(address(0x65), abi.encodePacked(bytes32(uint256(0xC0FFEE)), secondKey), 1); vm.prank(alice); mYieldToOne.transfer(bob, suint256(amount)); } - /* ============ Dual-Emit Regression — Infra transferFrom stays plaintext ============ */ - - function test_nativeTransferFrom_registeredRecipient_emitsPlaintextOnly() external { - // Regression: infra-gated native `transferFrom` MUST stay on the plaintext Transfer - // overload even when the recipient has a registered key. + function test_shieldedTransfer_zeroAmount_registeredRecipient() external { uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(alice, amount); @@ -1402,17 +1605,321 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(bob); mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, hex"deadbeefcafebabe"); + vm.prank(alice); - mYieldToOne.approve(address(swapFacility), suint256(amount)); + mYieldToOne.transfer(bob, suint256(0)); - assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + assertEq(mYieldToOne.getEncryptedEventNonce(), 1); + assertEq(mYieldToOne.getBalanceOf(alice), amount); + assertEq(mYieldToOne.getBalanceOf(bob), 0); + } - vm.recordLogs(); + function test_shieldedTransfer_unregisteredRecipient_emitsEmptyBytesAndSucceeds() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + // Key IS set: the ContractKeyNotSet check fires before the unregistered-recipient fallback. + _installContractKey(); + + // Precompiles are intentionally not mocked: the fallback path must not call them. + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, bytes("")); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), amount); + } + + function test_shieldedTransfer_zeroAmount_unregisteredRecipient() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, bytes("")); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(0)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + assertEq(mYieldToOne.getBalanceOf(alice), amount); + assertEq(mYieldToOne.getBalanceOf(bob), 0); + } + + function test_shieldedTransfer_contractKeyNotSet_reverts() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } + + function test_shieldedTransfer_contractKeyNotSet_unregisteredRecipient_reverts() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + // Reverts even for an unregistered recipient so success cannot leak who is registered. + vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + assertEq(mYieldToOne.getBalanceOf(alice), amount); + assertEq(mYieldToOne.getBalanceOf(bob), 0); + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + } + + /* ============ _ecdh / _hkdf / _aesGcmEncrypt ============ */ + + function test_shieldedTransfer_ecdhPrecompileFails() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.mockCallRevert(address(0x65), bytes(""), bytes("")); + + vm.expectRevert(abi.encodeWithSelector(IMYieldToOne.PrecompileFailed.selector, address(0x65))); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } + + function test_shieldedTransfer_hkdfPrecompileFails() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.mockCall(address(0x65), bytes(""), abi.encode(bytes32(uint256(1)))); + vm.mockCallRevert(address(0x68), bytes(""), bytes("")); + + vm.expectRevert(abi.encodeWithSelector(IMYieldToOne.PrecompileFailed.selector, address(0x68))); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } + + function test_shieldedTransfer_aesGcmPrecompileFails() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.mockCall(address(0x65), bytes(""), abi.encode(bytes32(uint256(1)))); + vm.mockCall(address(0x68), bytes(""), abi.encode(bytes32(uint256(2)))); + vm.mockCallRevert(address(0x66), bytes(""), bytes("")); + + vm.expectRevert(abi.encodeWithSelector(IMYieldToOne.PrecompileFailed.selector, address(0x66))); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } + + function test_shieldedApprove_ecdhPrecompileFails() external { + _installContractKey(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.mockCallRevert(address(0x65), bytes(""), bytes("")); + + vm.expectRevert(abi.encodeWithSelector(IMYieldToOne.PrecompileFailed.selector, address(0x65))); + + vm.prank(alice); + mYieldToOne.approve(bob, suint256(1_000e6)); + } + + /* ============ encryptedEventNonce ============ */ + + function test_encryptedEventNonce_sharedAcrossTransferAndApprove() external { + mYieldToOne.setBalanceOf(alice, 3_000e6); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(1_000e6)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 1); + + vm.prank(alice); + mYieldToOne.approve(bob, suint256(500e6)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 2); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(1_000e6)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 3); + } + + /* ============ _shieldedApprove ============ */ + + function test_shieldedApprove_registeredSpender_emitsBytesPayload() external { + uint256 amount = 1_000e6; + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.approve(bob, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 1); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Approval(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Approval(address,address,uint256)"); + + bool foundBytes; + bool foundPlaintext; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(mYieldToOne)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + assertEq(address(uint160(uint256(logs[i].topics[1]))), alice); + assertEq(address(uint160(uint256(logs[i].topics[2]))), bob); + bytes memory payload = abi.decode(logs[i].data, (bytes)); + assertGt(payload.length, 0); + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + } + } + + assertTrue(foundBytes, "missing Approval(address,address,bytes) emit"); + assertFalse(foundPlaintext, "plaintext Approval(uint256) emitted on shielded path"); + + assertEq(mYieldToOne.getShieldedAllowance(alice, bob), amount); + } + + function test_shieldedApprove_unregisteredSpender_emitsEmptyBytes() external { + uint256 amount = 1_000e6; + + _installContractKey(); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Approval(alice, bob, bytes("")); + + vm.prank(alice); + mYieldToOne.approve(bob, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + assertEq(mYieldToOne.getShieldedAllowance(alice, bob), amount); + } + + function test_shieldedApprove_contractKeyNotSet_reverts() external { + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + vm.prank(alice); + mYieldToOne.approve(bob, suint256(1_000e6)); + + vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + vm.prank(alice); + mYieldToOne.approve(carol, suint256(1_000e6)); + } + + function test_nativeApprove_emitsPlaintext() external { + uint256 amount = 1_000e6; + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(admin); + mYieldToOne.setAllowlisted(bob, true); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.approve(bob, amount); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Approval(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Approval(address,address,uint256)"); + + bool foundBytes; + bool foundPlaintext; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(mYieldToOne)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + assertEq(address(uint160(uint256(logs[i].topics[1]))), alice); + assertEq(address(uint160(uint256(logs[i].topics[2]))), bob); + assertEq(abi.decode(logs[i].data, (uint256)), amount); + } + } + + assertTrue(foundPlaintext, "missing plaintext Approval(uint256) emit on infra path"); + assertFalse(foundBytes, "infra path leaked into encrypted-bytes Approval overload"); + } + + /* ============ _shieldedTransfer (plaintext emit) ============ */ + + function test_nativeTransferFrom_registeredRecipient_emitsPlaintextOnly() external { + uint256 amount = 1_000e6; + mYieldToOne.setBalanceOf(alice, amount); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(alice); + mYieldToOne.approve(address(swapFacility), suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + + vm.recordLogs(); vm.prank(address(swapFacility)); mYieldToOne.transferFrom(alice, bob, amount); - // Encrypted path was never entered. assertEq(mYieldToOne.getEncryptedEventNonce(), 0); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -1440,11 +1947,9 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertFalse(foundBytes, "infra path leaked into encrypted-bytes Transfer overload"); } - /* ============ Dual-Emit Regression — Mint / Burn stay plaintext ============ */ + /* ============ _mint / _burn ============ */ function test_mint_emitsPlaintextOnly() external { - // _mint via SwapFacility.wrap MUST stay on the plaintext Transfer overload (bridge amounts - // are public) even with a contract key + recipient pubkey registered. uint256 amount = 1_000e6; mToken.setBalanceOf(address(swapFacility), amount); @@ -1461,7 +1966,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); mYieldToOne.wrap(alice, amount); - // Counter untouched: mint never enters the encrypted-emit path. assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -1477,7 +1981,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { if (logs[i].topics[0] == bytesTopic) { foundBytes = true; } else if (logs[i].topics[0] == plaintextTopic) { - // Mint: from == address(0). if (address(uint160(uint256(logs[i].topics[1]))) == address(0)) { foundPlaintextMint = true; assertEq(address(uint160(uint256(logs[i].topics[2]))), alice); @@ -1491,8 +1994,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { } function test_burn_emitsPlaintextOnly() external { - // _burn via SwapFacility.unwrap MUST stay on the plaintext Transfer overload even with a - // contract key + (notional) recipient pubkey registered. uint256 amount = 1_000e6; mYieldToOne.setBalanceOf(address(swapFacility), amount); @@ -1503,8 +2004,7 @@ contract MYieldToOneUnitTests is BaseUnitTest { _installContractKey(); _mockPrecompiles(); - // Register a pubkey for swapFacility to prove burn isn't routed through the encrypted - // path even when the source address has a registered key. + // swapFacility registers a key to prove burn still bypasses the encrypted path. vm.prank(address(swapFacility)); mYieldToOne.registerPublicKey(_validPubKey(0xCC)); @@ -1515,7 +2015,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { vm.prank(address(swapFacility)); mYieldToOne.unwrap(alice, amount); - // Counter untouched. assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -1531,7 +2030,6 @@ contract MYieldToOneUnitTests is BaseUnitTest { if (logs[i].topics[0] == bytesTopic) { foundBytes = true; } else if (logs[i].topics[0] == plaintextTopic) { - // Burn: to == address(0). if (address(uint160(uint256(logs[i].topics[2]))) == address(0)) { foundPlaintextBurn = true; assertEq(address(uint160(uint256(logs[i].topics[1]))), address(swapFacility)); @@ -1543,4 +2041,370 @@ contract MYieldToOneUnitTests is BaseUnitTest { assertTrue(foundPlaintextBurn, "missing plaintext Transfer(account, 0, amount) on burn"); assertFalse(foundBytes, "burn leaked into encrypted-bytes Transfer overload"); } + + /* ============ _encryptAmount (fuzz) ============ */ + + function testFuzz_shieldedTransfer_registeredRecipient_burnsExactlyOneNonce( + uint256 amount, + bool selfTransfer + ) external { + amount = bound(amount, 0, type(uint240).max); + + address recipient = selfTransfer ? alice : bob; + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(recipient); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + uint256 nonceBefore = mYieldToOne.getEncryptedEventNonce(); + + vm.prank(alice); + mYieldToOne.transfer(recipient, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore + 1); + } + + function testFuzz_shieldedTransfer_unregisteredRecipient_neverBurnsNonce(uint256 amount) external { + amount = bound(amount, 0, type(uint240).max); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + // Key IS set so the path is the unregistered-recipient fallback (empty ciphertext), not a revert. + _installContractKey(); + + uint256 nonceBefore = mYieldToOne.getEncryptedEventNonce(); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, bytes("")); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore); + } + + function testFuzz_shieldedTransfer_contractKeyNotSet_neverBurnsNonce(uint256 amount, bool registered) external { + amount = bound(amount, 1, type(uint240).max); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + if (registered) { + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + } + + vm.expectRevert(IMYieldToOne.ContractKeyNotSet.selector); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + assertEq(mYieldToOne.getEncryptedEventNonce(), 0); + assertEq(mYieldToOne.getBalanceOf(alice), type(uint240).max); + } + + function testFuzz_nativeTransferFrom_infraPath_neverBurnsNonce(uint256 amount) external { + amount = bound(amount, 1, type(uint240).max); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + _installContractKey(); + _mockPrecompiles(); + + // bob is registered; the infra (plaintext) path must still bypass the nonce. + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + vm.prank(alice); + mYieldToOne.approve(carol, suint256(type(uint256).max)); + + uint256 nonceBefore = mYieldToOne.getEncryptedEventNonce(); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, amount); + + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore); + } + + function testFuzz_shieldedTransfer_ciphertextMatchesPrecompileOutput(uint256 amount) external { + amount = bound(amount, 0, type(uint240).max); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Transfer(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Transfer(address,address,uint256)"); + + assertTrue(bytesTopic != plaintextTopic, "bytes overload topic0 collides with uint256 overload"); + + bool foundBytes; + bool foundPlaintext; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(mYieldToOne)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + assertEq(address(uint160(uint256(logs[i].topics[1]))), alice); + assertEq(address(uint160(uint256(logs[i].topics[2]))), bob); + assertEq(abi.decode(logs[i].data, (bytes)), hex"deadbeefcafebabe"); + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + } + } + + assertTrue(foundBytes, "missing Transfer(address,address,bytes) emit"); + assertFalse(foundPlaintext, "plaintext Transfer(uint256) emitted on shielded path"); + } + + function testFuzz_shieldedApprove_ciphertextMatchesPrecompileOutput(uint256 amount) external { + amount = bound(amount, 0, type(uint256).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.recordLogs(); + + vm.prank(alice); + mYieldToOne.approve(bob, suint256(amount)); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Approval(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Approval(address,address,uint256)"); + + assertTrue(bytesTopic != plaintextTopic, "bytes overload topic0 collides with uint256 overload"); + + bool foundBytes; + bool foundPlaintext; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(mYieldToOne)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + assertEq(address(uint160(uint256(logs[i].topics[1]))), alice); + assertEq(address(uint160(uint256(logs[i].topics[2]))), bob); + assertEq(abi.decode(logs[i].data, (bytes)), hex"deadbeefcafebabe"); + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + } + } + + assertTrue(foundBytes, "missing Approval(address,address,bytes) emit"); + assertFalse(foundPlaintext, "plaintext Approval(uint256) emitted on shielded path"); + + assertEq(mYieldToOne.getShieldedAllowance(alice, bob), amount); + } + + function testFuzz_encryptedEventNonce_strictlyIncrementsPerEmit(uint256 ops, uint256 seed) external { + ops = bound(ops, 1, 12); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + for (uint256 i; i < ops; ++i) { + uint256 nonceBefore = mYieldToOne.getEncryptedEventNonce(); + uint256 amount = uint256(keccak256(abi.encode(seed, i))) % 1_000e6; + + if (uint256(keccak256(abi.encode(seed, i, "kind"))) % 2 == 0) { + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(amount)); + } else { + vm.prank(alice); + mYieldToOne.approve(bob, suint256(amount)); + } + + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore + 1); + } + } + + /* ============ transferFrom (shielded) zero-amount ============ */ + + function test_shieldedTransferFrom_zeroAmount_registeredRecipient() external { + uint256 allowanceAmount = 1_500e6; + mYieldToOne.setBalanceOf(alice, 1_000e6); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(alice); + mYieldToOne.approve(carol, suint256(allowanceAmount)); + + uint256 nonceBefore = mYieldToOne.getEncryptedEventNonce(); + + // Encrypted-bytes overload still fires (the encrypt pipeline runs before the zero-amount early return). + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, hex"deadbeefcafebabe"); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(0)); + + // Allowance decremented by 0; a nonce is still burned; balances unchanged. + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), allowanceAmount); + assertEq(mYieldToOne.getEncryptedEventNonce(), nonceBefore + 1); + assertEq(mYieldToOne.getBalanceOf(alice), 1_000e6); + assertEq(mYieldToOne.getBalanceOf(bob), 0); + } + + /* ============ _spendAllowanceAndTransfer (fuzz) ============ */ + + function testFuzz_shieldedTransferFrom_allowanceDecrement( + uint256 allowanceAmount, + uint256 transferAmount, + bool infinite + ) external { + allowanceAmount = bound(allowanceAmount, 1, type(uint240).max); + transferAmount = bound(transferAmount, 0, allowanceAmount); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + mYieldToOne.setShieldedAllowance(alice, carol, infinite ? type(uint256).max : allowanceAmount); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(transferAmount)); + + assertEq( + mYieldToOne.getShieldedAllowance(alice, carol), + infinite ? type(uint256).max : allowanceAmount - transferAmount + ); + assertEq(mYieldToOne.getBalanceOf(bob), transferAmount); + } + + function testFuzz_shieldedTransferFrom_exactAllowanceBoundary(uint256 allowanceAmount) external { + allowanceAmount = bound(allowanceAmount, 1, type(uint240).max); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + mYieldToOne.setShieldedAllowance(alice, carol, allowanceAmount); + + _installContractKey(); + _mockPrecompiles(); + + // amount == allowance succeeds and zeroes the allowance. + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(allowanceAmount)); + + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), 0); + assertEq(mYieldToOne.getBalanceOf(bob), allowanceAmount); + } + + function testFuzz_shieldedTransferFrom_overAllowanceBoundaryReverts(uint256 allowanceAmount) external { + allowanceAmount = bound(allowanceAmount, 0, type(uint240).max - 1); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + mYieldToOne.setShieldedAllowance(alice, carol, allowanceAmount); + + // amount == allowance + 1 reverts with a zeroed payload and leaves the allowance untouched. + vm.expectRevert( + abi.encodeWithSelector(IERC20Extended.InsufficientAllowance.selector, carol, 0, allowanceAmount + 1) + ); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(allowanceAmount + 1)); + + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), allowanceAmount); + assertEq(mYieldToOne.getBalanceOf(bob), 0); + } + + function testFuzz_shieldedTransferFrom_residualAllowanceSpendableAfterDelisting( + uint256 allowanceAmount, + uint256 firstSpend, + uint256 secondSpend + ) external { + allowanceAmount = bound(allowanceAmount, 2, type(uint240).max); + firstSpend = bound(firstSpend, 1, allowanceAmount - 1); + secondSpend = bound(secondSpend, 0, allowanceAmount - firstSpend); + + mYieldToOne.setBalanceOf(alice, type(uint240).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, true); + + // Grant while allowlisted, then de-list: the residual allowance must remain spendable via shielded transferFrom. + vm.prank(alice); + mYieldToOne.approve(carol, suint256(allowanceAmount)); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(firstSpend)); + + vm.prank(admin); + mYieldToOne.setAllowlisted(carol, false); + + vm.prank(carol); + mYieldToOne.transferFrom(alice, bob, suint256(secondSpend)); + + assertEq(mYieldToOne.getShieldedAllowance(alice, carol), allowanceAmount - firstSpend - secondSpend); + assertEq(mYieldToOne.getBalanceOf(bob), firstSpend + secondSpend); + } + + /* ============ boundary values ============ */ + + function test_transfer_maxUintAmount() external { + mYieldToOne.setBalanceOf(alice, type(uint256).max); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOne.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(alice); + mYieldToOne.transfer(bob, suint256(type(uint256).max)); + + assertEq(mYieldToOne.getBalanceOf(alice), 0); + assertEq(mYieldToOne.getBalanceOf(bob), type(uint256).max); + assertEq(mYieldToOne.getEncryptedEventNonce(), 1); + } + + function test_approve_maxUintAmount() external { + _installContractKey(); + + // type(uint256).max is the infinite, non-decrementing allowance sentinel. + vm.prank(alice); + mYieldToOne.approve(bob, suint256(type(uint256).max)); + + assertEq(mYieldToOne.getShieldedAllowance(alice, bob), type(uint256).max); + + vm.prank(alice); + assertEq(mYieldToOne.allowance(alice, bob), type(uint256).max); + } + + function test_balanceOf_maxUintBoundary() external { + mYieldToOne.setBalanceOf(alice, type(uint256).max); + + vm.prank(alice); + assertEq(mYieldToOne.balanceOf(alice), type(uint256).max); + } } diff --git a/test/unit/projects/yieldToOne/MYieldToOneForcedTransfer.t.sol b/test/unit/projects/yieldToOne/MYieldToOneForcedTransfer.t.sol index ada4dece..e3718924 100644 --- a/test/unit/projects/yieldToOne/MYieldToOneForcedTransfer.t.sol +++ b/test/unit/projects/yieldToOne/MYieldToOneForcedTransfer.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.26; +import { Vm } from "../../../../lib/forge-std/src/Vm.sol"; + import { IERC20 } from "../../../../lib/common/src/interfaces/IERC20.sol"; import { IAccessControl } from "../../../../lib/common/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; @@ -184,6 +186,77 @@ contract MYieldToOneForcedTransferUnitTest is BaseUnitTest { } } + function test_forceTransfer_registeredRecipient_emitsPlaintextOnly() public { + uint256 amount = 1_000e6; + mYieldToOneForcedTransfer.setBalanceOf(alice, amount); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOneForcedTransfer.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(freezeManager); + mYieldToOneForcedTransfer.freeze(alice); + + assertEq(mYieldToOneForcedTransfer.getEncryptedEventNonce(), 0); + + vm.recordLogs(); + + vm.prank(forcedTransferManager); + mYieldToOneForcedTransfer.forceTransfer(alice, bob, amount); + + assertEq(mYieldToOneForcedTransfer.getEncryptedEventNonce(), 0); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 bytesTopic = keccak256("Transfer(address,address,bytes)"); + bytes32 plaintextTopic = keccak256("Transfer(address,address,uint256)"); + bytes32 forcedTopic = keccak256("ForcedTransfer(address,address,address,uint256)"); + + bool foundBytes; + bool foundPlaintext; + bool foundForced; + for (uint256 i; i < logs.length; ++i) { + if (logs[i].emitter != address(mYieldToOneForcedTransfer)) continue; + if (logs[i].topics.length == 0) continue; + + if (logs[i].topics[0] == bytesTopic) { + foundBytes = true; + } else if (logs[i].topics[0] == plaintextTopic) { + foundPlaintext = true; + assertEq(address(uint160(uint256(logs[i].topics[1]))), alice); + assertEq(address(uint160(uint256(logs[i].topics[2]))), bob); + assertEq(abi.decode(logs[i].data, (uint256)), amount); + } else if (logs[i].topics[0] == forcedTopic) { + foundForced = true; + } + } + + assertTrue(foundPlaintext, "missing plaintext Transfer(uint256) emit on forceTransfer"); + assertTrue(foundForced, "missing ForcedTransfer emit"); + assertFalse(foundBytes, "forceTransfer leaked into encrypted-bytes Transfer overload"); + + assertEq(mYieldToOneForcedTransfer.getBalanceOf(alice), 0); + assertEq(mYieldToOneForcedTransfer.getBalanceOf(bob), amount); + } + + function test_forceTransfer_worksWhilePaused() public { + uint256 amount = 1_000e6; + mYieldToOneForcedTransfer.setBalanceOf(alice, amount); + + vm.prank(freezeManager); + mYieldToOneForcedTransfer.freeze(alice); + + vm.prank(pauser); + mYieldToOneForcedTransfer.pause(); + + vm.prank(forcedTransferManager); + mYieldToOneForcedTransfer.forceTransfer(alice, bob, amount); + + assertEq(mYieldToOneForcedTransfer.getBalanceOf(alice), 0); + assertEq(mYieldToOneForcedTransfer.getBalanceOf(bob), amount); + } + /* ============ forceTransfers ============ */ function test_forceTransfers_succeedsForManager() public { @@ -348,12 +421,9 @@ contract MYieldToOneForcedTransferUnitTest is BaseUnitTest { } } - /* ============ inherited native infra paths ============ */ + /* ============ transferFrom (native) ============ */ function test_nativeTransferFrom_allowlistedCaller() public { - // The FT subclass does not override the native, allowlist-gated `approve` / `transferFrom` - // paths, so it inherits them from MYieldToOne. An allowlisted caller can drive a native - // `transferFrom(uint256)` against the shared shielded allowance slot. uint256 amount = 1_000e6; mYieldToOneForcedTransfer.setBalanceOf(alice, amount); @@ -387,6 +457,8 @@ contract MYieldToOneForcedTransferUnitTest is BaseUnitTest { mYieldToOneForcedTransfer.transferFrom(alice, bob, amount); } + /* ============ balanceOf ============ */ + function test_balanceOf_allowlistedInfraCanReadAnyHolder() public { mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); @@ -397,6 +469,144 @@ contract MYieldToOneForcedTransferUnitTest is BaseUnitTest { assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 1_000e6); } + function test_balanceOf_holderCanRead() public { + mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); + + vm.prank(alice); + assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 1_000e6); + } + + function test_balanceOf_freezeManagerCanRead() public { + mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); + + vm.prank(freezeManager); + assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 1_000e6); + } + + function test_balanceOf_forcedTransferManagerCanRead() public { + mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); + + vm.prank(forcedTransferManager); + assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 1_000e6); + } + + function test_balanceOf_unauthorized() public { + mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); + + vm.expectRevert(IMYieldToOne.Unauthorized.selector); + vm.prank(bob); + mYieldToOneForcedTransfer.balanceOf(alice); + } + + function test_forceTransfer_seizureSizedByBalanceOf() public { + mYieldToOneForcedTransfer.setBalanceOf(alice, 1_000e6); + + vm.prank(freezeManager); + mYieldToOneForcedTransfer.freeze(alice); + + vm.prank(forcedTransferManager); + uint256 seized = mYieldToOneForcedTransfer.balanceOf(alice); + + assertEq(seized, 1_000e6); + + vm.prank(forcedTransferManager); + mYieldToOneForcedTransfer.forceTransfer(alice, bob, seized); + + vm.prank(forcedTransferManager); + assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 0); + + vm.prank(forcedTransferManager); + assertEq(mYieldToOneForcedTransfer.balanceOf(bob), seized); + } + + /* ============ Helpers ============ */ + + function _validPubKey(bytes1 marker) internal pure returns (bytes memory) { + bytes memory key = new bytes(33); + key[0] = 0x02; + for (uint256 i = 1; i < 33; ++i) { + key[i] = marker; + } + return key; + } + + function _mockPrecompiles() internal { + vm.mockCall(address(0x65), bytes(""), abi.encode(bytes32(uint256(1)))); + vm.mockCall(address(0x68), bytes(""), abi.encode(bytes32(uint256(2)))); + vm.mockCall(address(0x66), bytes(""), hex"deadbeefcafebabe"); + } + + function _installContractKey() internal { + vm.prank(admin); + mYieldToOneForcedTransfer.setContractKey(sbytes32(bytes32(uint256(0xC0FFEE))), _validPubKey(0xAA)); + } + + /* ============ transfer / transferFrom / approve (shielded) ============ */ + + function test_transfer_shieldedOverload() external { + uint256 amount = 1_000e6; + mYieldToOneForcedTransfer.setBalanceOf(alice, amount); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOneForcedTransfer.registerPublicKey(_validPubKey(0xBB)); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, hex"deadbeefcafebabe"); + + vm.prank(alice); + mYieldToOneForcedTransfer.transfer(bob, suint256(amount)); + + assertEq(mYieldToOneForcedTransfer.getEncryptedEventNonce(), 1); + assertEq(mYieldToOneForcedTransfer.getBalanceOf(alice), 0); + assertEq(mYieldToOneForcedTransfer.getBalanceOf(bob), amount); + } + + function test_transferFrom_shieldedOverload() external { + uint256 amount = 1_000e6; + mYieldToOneForcedTransfer.setBalanceOf(alice, amount); + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOneForcedTransfer.registerPublicKey(_validPubKey(0xBB)); + + vm.prank(alice); + mYieldToOneForcedTransfer.approve(carol, suint256(amount)); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Transfer(alice, bob, hex"deadbeefcafebabe"); + + vm.prank(carol); + mYieldToOneForcedTransfer.transferFrom(alice, bob, suint256(amount)); + + assertEq(mYieldToOneForcedTransfer.getBalanceOf(alice), 0); + assertEq(mYieldToOneForcedTransfer.getBalanceOf(bob), amount); + assertEq(mYieldToOneForcedTransfer.getShieldedAllowance(alice, carol), 0); + } + + function test_approve_shieldedOverload() external { + uint256 amount = 1_000e6; + + _installContractKey(); + _mockPrecompiles(); + + vm.prank(bob); + mYieldToOneForcedTransfer.registerPublicKey(_validPubKey(0xBB)); + + vm.expectEmit(true, true, false, true); + emit IMYieldToOne.Approval(alice, bob, hex"deadbeefcafebabe"); + + vm.prank(alice); + mYieldToOneForcedTransfer.approve(bob, suint256(amount)); + + assertEq(mYieldToOneForcedTransfer.getEncryptedEventNonce(), 1); + assertEq(mYieldToOneForcedTransfer.getShieldedAllowance(alice, bob), amount); + } + /* ============ claimYield ============ */ function test_claimYield_noYield() external { diff --git a/test/unit/projects/yieldToOne/MYieldToOneSimulation.t.sol b/test/unit/projects/yieldToOne/MYieldToOneSimulation.t.sol new file mode 100644 index 00000000..1f69635b --- /dev/null +++ b/test/unit/projects/yieldToOne/MYieldToOneSimulation.t.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +import { Upgrades } from "../../../../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; + +import { MYieldToOneForcedTransfer } from "../../../../src/projects/yieldToOne/MYieldToOneForcedTransfer.sol"; +import { IMYieldToOne } from "../../../../src/projects/yieldToOne/interfaces/IMYieldToOne.sol"; + +import { ISwapFacility } from "../../../../src/swap/interfaces/ISwapFacility.sol"; + +import { MYieldToOneForcedTransferHarness } from "../../../harness/MYieldToOneForcedTransferHarness.sol"; +import { BaseUnitTest } from "../../../utils/BaseUnitTest.sol"; + +contract MYieldToOneSimulationTests is BaseUnitTest { + uint256 internal constant _OPS_PER_RUN = 200; + uint256 internal constant _MAX_WRAP_AMOUNT = 1e15; + uint256 internal constant _MAX_YIELD_DELTA = 1e12; + + MYieldToOneForcedTransferHarness public mYieldToOne; + + address public infra = makeAddr("infra"); + + address[] public holders; + + uint256 internal _lastNonce; + + function setUp() public override { + super.setUp(); + + mYieldToOne = MYieldToOneForcedTransferHarness( + Upgrades.deployTransparentProxy( + "MYieldToOneForcedTransferHarness.sol:MYieldToOneForcedTransferHarness", + admin, + abi.encodeWithSelector( + MYieldToOneForcedTransfer.initialize.selector, + "HALO USD", + "HALO USD", + yieldRecipient, + admin, + freezeManager, + yieldRecipientManager, + pauser, + forcedTransferManager + ), + mExtensionDeployOptions + ) + ); + + registrar.setEarner(address(mYieldToOne), true); + + vm.prank(admin); + mYieldToOne.setAllowlisted(infra, true); + + // Widened beyond the balance-holding actors: `infra` (allowlisted) and `carol` (untouched, unregistered) + // must both stay at zero, exercising the gate/accounting checks against non-holders too. + holders = [alice, bob, charlie, david, yieldRecipient, address(swapFacility), infra, carol]; + } + + /* ============ Simulation ============ */ + + /// forge-config: default.fuzz.runs = 1000 + /// forge-config: seismic.fuzz.runs = 1000 + function testFuzz_simulation(uint256 seed) external { + _installContractKey(); + _mockPrecompiles(); + + for (uint256 i; i < _OPS_PER_RUN; ++i) { + address actor = accounts[(seed = _getNewSeed(seed)) % accounts.length]; + address peer = accounts[(seed = _getNewSeed(seed)) % accounts.length]; + + // 10% chance to drip yield into the mock M token. + if ((seed = _getNewSeed(seed)) % 100 < 10) { + mToken.setBalanceOf( + address(mYieldToOne), + mToken.balanceOf(address(mYieldToOne)) + (_getNewSeed(seed) % _MAX_YIELD_DELTA) + ); + } + + uint256 op = (seed = _getNewSeed(seed)) % 100; + + if (op < 12) _wrap((seed = _getNewSeed(seed)), actor, peer); + else if (op < 22) _unwrap((seed = _getNewSeed(seed)), actor); + else if (op < 37) _transfer((seed = _getNewSeed(seed)), actor, peer); + else if (op < 47) _transferFromShielded((seed = _getNewSeed(seed)), actor, peer, false); + else if (op < 55) _transferFromShielded((seed = _getNewSeed(seed)), actor, peer, true); + else if (op < 63) _transferFromNative((seed = _getNewSeed(seed)), actor, peer); + else if (op < 71) _claimYield(actor); + else if (op < 76) _setYieldRecipient((seed = _getNewSeed(seed))); + else if (op < 83) _forceTransfer((seed = _getNewSeed(seed)), peer); + else if (op < 91) _toggleFreeze((seed = _getNewSeed(seed)), actor); + else if (op < 95) _togglePause((seed = _getNewSeed(seed))); + else _registerPublicKey((seed = _getNewSeed(seed)), actor); + + _checkInvariants(); + } + } + + /* ============ Ops ============ */ + + function _wrap(uint256 seed, address actor, address recipient) internal { + if (mYieldToOne.paused() || mYieldToOne.isFrozen(actor) || mYieldToOne.isFrozen(recipient)) return; + + uint256 amount = bound(seed, 1, _MAX_WRAP_AMOUNT); + + mToken.setBalanceOf(address(swapFacility), mToken.balanceOf(address(swapFacility)) + amount); + + vm.mockCall(address(swapFacility), abi.encodeWithSelector(ISwapFacility.msgSender.selector), abi.encode(actor)); + + vm.prank(address(swapFacility)); + mYieldToOne.wrap(recipient, amount); + } + + function _unwrap(uint256 seed, address actor) internal { + if (mYieldToOne.paused() || mYieldToOne.isFrozen(actor)) return; + + uint256 balance = mYieldToOne.getBalanceOf(actor); + + if (balance == 0) return; + + uint256 amount = bound(seed, 1, balance); + + // Mirrors the production unwrap route through SwapFacility. + vm.prank(actor); + mYieldToOne.transfer(address(swapFacility), suint256(amount)); + + vm.mockCall(address(swapFacility), abi.encodeWithSelector(ISwapFacility.msgSender.selector), abi.encode(actor)); + + vm.prank(address(swapFacility)); + mYieldToOne.unwrap(actor, amount); + + vm.prank(address(swapFacility)); + mToken.transfer(actor, amount); + } + + function _transfer(uint256 seed, address actor, address recipient) internal { + if (mYieldToOne.paused() || mYieldToOne.isFrozen(actor) || mYieldToOne.isFrozen(recipient)) return; + + uint256 amount = bound(seed, 0, mYieldToOne.getBalanceOf(actor)); + + vm.prank(actor); + mYieldToOne.transfer(recipient, suint256(amount)); + } + + function _transferFromShielded(uint256 seed, address owner, address recipient, bool infinite) internal { + address spender = accounts[_getNewSeed(seed) % accounts.length]; + + if (mYieldToOne.paused()) return; + if (mYieldToOne.isFrozen(owner) || mYieldToOne.isFrozen(spender) || mYieldToOne.isFrozen(recipient)) return; + + uint256 amount = bound(seed, 0, mYieldToOne.getBalanceOf(owner)); + + vm.prank(owner); + mYieldToOne.approve(spender, infinite ? suint256(type(uint256).max) : suint256(amount)); + + vm.prank(spender); + mYieldToOne.transferFrom(owner, recipient, suint256(amount)); + } + + function _transferFromNative(uint256 seed, address owner, address recipient) internal { + if (mYieldToOne.paused() || mYieldToOne.isFrozen(owner) || mYieldToOne.isFrozen(recipient)) return; + + uint256 amount = bound(seed, 0, mYieldToOne.getBalanceOf(owner)); + + vm.prank(owner); + mYieldToOne.approve(infra, amount); + + vm.prank(infra); + mYieldToOne.transferFrom(owner, recipient, amount); + } + + function _claimYield(address actor) internal { + if (mYieldToOne.paused()) return; + + vm.prank(actor); + mYieldToOne.claimYield(); + } + + function _setYieldRecipient(uint256 seed) internal { + address target = seed % 5 == 0 ? yieldRecipient : accounts[seed % accounts.length]; + + vm.prank(yieldRecipientManager); + mYieldToOne.setYieldRecipient(target); + } + + function _forceTransfer(uint256 seed, address recipient) internal { + for (uint256 i; i < accounts.length; ++i) { + if (!mYieldToOne.isFrozen(accounts[i])) continue; + + uint256 amount = bound(seed, 0, mYieldToOne.getBalanceOf(accounts[i])); + + vm.prank(forcedTransferManager); + mYieldToOne.forceTransfer(accounts[i], recipient, amount); + + return; + } + } + + function _toggleFreeze(uint256 seed, address actor) internal { + if (mYieldToOne.isFrozen(actor)) { + vm.prank(freezeManager); + mYieldToOne.unfreeze(actor); + } else if (seed % 3 == 0) { + vm.prank(freezeManager); + mYieldToOne.freeze(actor); + } + } + + function _togglePause(uint256 seed) internal { + if (mYieldToOne.paused()) { + vm.prank(pauser); + mYieldToOne.unpause(); + } else if (seed % 4 == 0) { + vm.prank(pauser); + mYieldToOne.pause(); + } + } + + function _registerPublicKey(uint256 seed, address actor) internal { + vm.prank(actor); + mYieldToOne.registerPublicKey(_validPubKey(bytes1(uint8(seed)))); + } + + /* ============ Invariants ============ */ + + function _checkInvariants() internal { + uint256 sum; + for (uint256 i; i < holders.length; ++i) { + uint256 balance = mYieldToOne.getBalanceOf(holders[i]); + + // No individual balance may exceed totalSupply: a silent underflow would wrap a balance + // far past totalSupply and trip here before the sum check could mask it. + assertLe( + balance, + mYieldToOne.totalSupply(), + "Invariant 4 Failed: holder balance > totalSupply (underflow)" + ); + + sum += balance; + } + + assertEq(sum, mYieldToOne.totalSupply(), "Invariant 1 Failed: sum of balances != totalSupply"); + + assertGe( + mToken.balanceOf(address(mYieldToOne)), + mYieldToOne.totalSupply(), + "Invariant 2 Failed: M balance < totalSupply" + ); + + uint256 nonce = mYieldToOne.getEncryptedEventNonce(); + assertGe(nonce, _lastNonce, "Invariant 3 Failed: encrypted event nonce decreased"); + _lastNonce = nonce; + + // The shielded `balanceOf` gate must hold: `carol` is never granted a role, allowlisted, or made a holder, + // so a read of any holder by `carol` must revert `Unauthorized` regardless of the action sequence. + vm.prank(carol); + vm.expectRevert(IMYieldToOne.Unauthorized.selector); + mYieldToOne.balanceOf(alice); + + // Allowance accounting must never leave a finite allowance dangling above the infinite sentinel, + // and the gated read must succeed for the owner: `alice -> infra` is read by `alice` and stays valid. + vm.prank(alice); + uint256 infraAllowance = mYieldToOne.allowance(alice, infra); + assertLe(infraAllowance, type(uint256).max, "Invariant 5 Failed: allowance exceeds uint256 max"); + } + + /* ============ Helpers ============ */ + + function _validPubKey(bytes1 marker) internal pure returns (bytes memory) { + bytes memory key = new bytes(33); + key[0] = 0x02; + for (uint256 i = 1; i < 33; ++i) { + key[i] = marker; + } + return key; + } + + function _installContractKey() internal { + vm.prank(admin); + mYieldToOne.setContractKey(sbytes32(bytes32(uint256(0xC0FFEE))), _validPubKey(0xAA)); + } + + function _mockPrecompiles() internal { + vm.mockCall(address(0x65), bytes(""), abi.encode(bytes32(uint256(1)))); + vm.mockCall(address(0x68), bytes(""), abi.encode(bytes32(uint256(2)))); + vm.mockCall(address(0x66), bytes(""), hex"deadbeefcafebabe"); + } + + function _getNewSeed(uint256 seed) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed))); + } +}