ipor-fusion 4.0.0.dev191__tar.gz → 4.0.0.dev200__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/AGENTS.md +14 -1
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/MIGRATION_4.md +34 -1
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/PKG-INFO +1 -1
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/vault_health.py +14 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/plasma_vault.py +28 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/mcp/models.py +26 -1
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/lending_health.py +2 -9
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/morpho.py +26 -16
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/types.py +1 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/validation.py +94 -9
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_data.py +50 -4
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_info.py +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/clean.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/clean.mcp.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/deployment_error.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/deployment_error.mcp.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/fetched.cli.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/fetched.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/fetched.mcp.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/full.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/full.mcp.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/rich.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/rich.mcp.json +10 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_commands.py +36 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_lending_health.py +47 -21
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_vault_predates_getters.py +54 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_mcp_models.py +2 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_plasma_vault.py +31 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_reader_encoding.py +11 -8
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_validation.py +128 -0
- ipor_fusion-4.0.0.dev200/tests/test_vault_reconciliation_onchain_base.py +65 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/README.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cache.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/vault_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/vault_info_view.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/access.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/context.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/contract.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/fuse.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/multicall.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/simulation_result.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/discovery.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/errors.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/explorer.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/mcp/server.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/morpho_api.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_managers.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_positions.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/vault_reads.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_event_args.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_fake_rpc.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_golden.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_oracle_mapping_fake.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/conftest.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/meta_morpho_v1.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/meta_morpho_v2.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_api/market.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_api/vault_v1.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_api/vault_v2.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue_api_error.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue_no_api.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/oracle_mapping_golden/mapping.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/simulate_v1_request.json +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/clean.txt +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/deployment_error.txt +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/full.txt +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/fixtures/vault_info_golden/rich.txt +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cache.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_helpers.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_market_golden.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_market_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_oracle_mapping_golden.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_pending_requests_scan.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_vault_info_golden.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_contract_call.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_ccip_indexer.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_ccip_quote.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_lane.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_crosschain_transport.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_docs_typing.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_error_hierarchy.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_explorer.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_fusion_instance.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_live.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_readers.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_mcp_server.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_public_api.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_public_type_reachability.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulation_errors.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_simulation_observation.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_data_block.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_info_fetched_golden.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_vault_simulator_blocks.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/typed_client/consumer.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/tests/typed_client/rejected.py +0 -0
- {ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/uv.lock +0 -0
|
@@ -513,7 +513,20 @@ decoder adds its own fields.
|
|
|
513
513
|
- `ZeroBalanceFuse` markets are capabilities (swap, flash-loan,
|
|
514
514
|
instant-withdrawal, admin) with structurally zero balance, not venues. They
|
|
515
515
|
go in `zero_balance_fuses` and are not counted as liquidity markets.
|
|
516
|
-
- Reconciliation: `totalAssets = balance_fuses_sum + underlying_on_vault
|
|
516
|
+
- Reconciliation: `totalAssets = balance_fuses_sum + underlying_on_vault +
|
|
517
|
+
rewards_manager_balance` (`PlasmaVault._getGrossTotalAssets` adds
|
|
518
|
+
`IRewardsClaimManager.balanceOf()`, the vested rewards, when a manager is set;
|
|
519
|
+
0 without one; a manager or balance that cannot be read is a
|
|
520
|
+
`rewards_manager_balance` warning, and pre-rename vaults answer
|
|
521
|
+
`getRewardsManagerAddress()`). Vaults built from contracts before ipor-fusion
|
|
522
|
+
#475 (Feb 2026), the August 2024 audit generation included, return gross minus
|
|
523
|
+
the unrealized management fee; later ones return gross. The deployed version
|
|
524
|
+
is not readable, so the reconciliation decides by behavior: it nets the fee
|
|
525
|
+
(`management_fee_netted`) when `totalAssets` equals `max(0, gross - fee)` and not
|
|
526
|
+
`gross`, with gross built from the stored global market total
|
|
527
|
+
(`PlasmaVault.read_stored_markets_total`, an ERC-7201 slot), never from the
|
|
528
|
+
per-market caches.
|
|
529
|
+
Never key it on the legacy-getter or pre-audit detection.
|
|
517
530
|
Balance fuses already price non-underlying ERC20s (e.g. ERC20BalanceFuse
|
|
518
531
|
values BOLD + BOLDUSDC-gauge in WETH terms). Never add all ERC20 values on
|
|
519
532
|
top of balance fuse totals; that double-counts. `erc20_direct_total` in JSON
|
|
@@ -639,7 +639,7 @@ converted amounts `Amount`. A custom `VaultState` annotates the same.
|
|
|
639
639
|
`Shares` (vault shares), and `transfer` / `approve` / `transfer_from` /
|
|
640
640
|
`set_total_supply_cap` take `Shares`; `ERC20` keeps `Amount`.
|
|
641
641
|
`Reconciliation` amounts (`balance_fuses_total`, `underlying_on_vault`,
|
|
642
|
-
`erc20_direct_total`, `sum`, `on_chain_total_assets`, `delta`,
|
|
642
|
+
`rewards_manager_balance`, `erc20_direct_total`, `sum`, `on_chain_total_assets`, `delta`,
|
|
643
643
|
`pending_withdrawals`, `implied_market_total`, `market_storage_divergence`) are
|
|
644
644
|
`Amount`. `FusionInstance.asset_decimals` / `underlying_token_decimals` are
|
|
645
645
|
`Decimals`, and `CloneArgs.redemption_delay_seconds` is a `Period`.
|
|
@@ -950,6 +950,39 @@ and `log_index` was passed through as given (a hex string from a raw log); the e
|
|
|
950
950
|
These apply to every caller, type-checked or not. Changes described in full above
|
|
951
951
|
are listed with a link.
|
|
952
952
|
|
|
953
|
+
- **The reconciliation counts the rewards claim manager.** `PlasmaVault.totalAssets`
|
|
954
|
+
adds `RewardsClaimManager.balanceOf()` (vested rewards) to the vault's idle
|
|
955
|
+
underlying and its markets; 3.x left it out, so a vault with vested rewards showed
|
|
956
|
+
a delta of that size and an inflated `implied_market_total` (a false market-storage
|
|
957
|
+
drift). `fetch_vault_data` reads it into `VaultData.rewards_manager_balance` (0
|
|
958
|
+
without a manager, `None` when the read fails, which validation reports as a
|
|
959
|
+
`rewards_manager_balance` warning), and `Reconciliation`, the `vault info` and
|
|
960
|
+
`vault validate` documents carry a `rewards_manager_balance` amount. A custom
|
|
961
|
+
`VaultState` adds the `rewards_manager_balance` property, and code constructing a
|
|
962
|
+
`Reconciliation` passes `rewards_manager_balance` and `rewards_manager_balance_usd`.
|
|
963
|
+
|
|
964
|
+
- **The reconciliation follows the vault's `totalAssets` formula.** Contracts before
|
|
965
|
+
ipor-fusion #475 (Feb 2026) return gross assets minus the unrealized management
|
|
966
|
+
fee, later ones gross. 3.x always compared gross, so every older vault with an
|
|
967
|
+
accrued fee showed that fee as a delta. The reconciliation now subtracts the
|
|
968
|
+
fee (`Reconciliation.management_fee_netted`, also in both JSON documents) when
|
|
969
|
+
`totalAssets` equals `max(0, gross - fee)` rather than `gross`, gross built from
|
|
970
|
+
the global market total the vault stores (`VaultData.stored_markets_total`, read
|
|
971
|
+
by `PlasmaVault.read_stored_markets_total`); the vault's age plays no part. A
|
|
972
|
+
custom `VaultState` adds the `unrealized_management_fee` (`VaultData` derives it
|
|
973
|
+
from `fee_data`) and `stored_markets_total` properties; code constructing a
|
|
974
|
+
`Reconciliation` passes `management_fee_netted` and `management_fee_netted_usd`.
|
|
975
|
+
|
|
976
|
+
- **Morpho shares convert to assets as Morpho Blue does.** The position breakdown
|
|
977
|
+
(`MorphoReader.read_position_breakdown`, the `vault info` Morpho rows) and the Morpho
|
|
978
|
+
lending health follow `SharesMathLib`: integer math with 1e6 virtual shares and one
|
|
979
|
+
virtual asset, borrow rounded up, supply rounded down, on the market totals Morpho
|
|
980
|
+
stores (interest accrued since the market's last update is not included). 3.x
|
|
981
|
+
converted borrow shares by dividing by `total_shares + 1` in floating point and
|
|
982
|
+
supply shares without the virtual offsets, so large positions lost their low digits
|
|
983
|
+
and small or empty markets could be off by orders of magnitude: expect materially
|
|
984
|
+
different numbers where they were wrong.
|
|
985
|
+
|
|
953
986
|
- **MCP `outputSchema` describes every nested block.** The tool output is
|
|
954
987
|
byte-identical; the schemas of `vault_info`, `vault_oracle_mapping`,
|
|
955
988
|
`market_morpho_blue` and `market_meta_morpho` replace each free-form object with
|
|
@@ -73,6 +73,20 @@ def _print_reconciliation(data: VaultData, validation: VaultValidation) -> None:
|
|
|
73
73
|
click.echo(
|
|
74
74
|
f" Underlying on vault: {fmt_underlying} {sym}{usd_underlying} [{sym} held directly]"
|
|
75
75
|
)
|
|
76
|
+
if recon.rewards_manager_balance > 0:
|
|
77
|
+
fmt_rewards = _format_amount(recon.rewards_manager_balance, decimals)
|
|
78
|
+
usd_rewards = f" (${recon.rewards_manager_balance_usd:,.2f})" if price else ""
|
|
79
|
+
click.echo(
|
|
80
|
+
f" Rewards manager: {fmt_rewards} {sym}{usd_rewards}"
|
|
81
|
+
" [vested rewards, in totalAssets]"
|
|
82
|
+
)
|
|
83
|
+
if recon.management_fee_netted > 0:
|
|
84
|
+
fmt_fee = _format_amount(recon.management_fee_netted, decimals)
|
|
85
|
+
usd_fee = f" (${recon.management_fee_netted_usd:,.2f})" if price else ""
|
|
86
|
+
click.echo(
|
|
87
|
+
f" Management fee: -{fmt_fee} {sym}{usd_fee}"
|
|
88
|
+
" [unrealized, netted by totalAssets]"
|
|
89
|
+
)
|
|
76
90
|
click.echo(f" Sum: {fmt_sum} {sym}{usd_sum}")
|
|
77
91
|
click.echo(f" On-chain totalAssets: {fmt_onchain} {sym}{usd_onchain}")
|
|
78
92
|
sign = "+" if recon.delta >= 0 else "-"
|
|
@@ -90,6 +90,15 @@ def _market_id_list_decoder(value: tuple[int, ...]) -> list[MarketId]:
|
|
|
90
90
|
|
|
91
91
|
_BALANCE_OF_SELECTOR = function_signature_to_4byte_selector("balanceOf()")
|
|
92
92
|
|
|
93
|
+
# PlasmaVaultStorageLib.PLASMA_VAULT_TOTAL_ASSETS_IN_ALL_MARKETS, and its value
|
|
94
|
+
# before the June 2024 storage rename (ipor-fusion #43).
|
|
95
|
+
_TOTAL_ASSETS_IN_ALL_MARKETS_SLOT = int(
|
|
96
|
+
"0x24e02552e88772b8e8fd15f3e6699ba530635ffc6b52322da922b0b497a77300", 16
|
|
97
|
+
)
|
|
98
|
+
_LEGACY_TOTAL_ASSETS_SLOT = int(
|
|
99
|
+
"0xf660efede6b8071aab61a75191de57a4a8a416b088f961d8c2a18168aa7c7700", 16
|
|
100
|
+
)
|
|
101
|
+
|
|
93
102
|
|
|
94
103
|
def _read_result_data(result: tuple[bytes]) -> bytes:
|
|
95
104
|
"""``ReadResult.data``, undecoded."""
|
|
@@ -449,6 +458,15 @@ class PlasmaVault(ContractWrapper):
|
|
|
449
458
|
decoder=Web3.to_checksum_address,
|
|
450
459
|
)
|
|
451
460
|
|
|
461
|
+
def get_rewards_manager_address(self) -> Call[ChecksumAddress]:
|
|
462
|
+
"""The getter of vaults deployed before the manager was renamed
|
|
463
|
+
RewardsClaimManager (June 2024)."""
|
|
464
|
+
return self._view(
|
|
465
|
+
"getRewardsManagerAddress()",
|
|
466
|
+
output_types=["address"],
|
|
467
|
+
decoder=Web3.to_checksum_address,
|
|
468
|
+
)
|
|
469
|
+
|
|
452
470
|
def get_performance_fee_data(self) -> Call[PerformanceFeeData]:
|
|
453
471
|
return self._view(
|
|
454
472
|
"getPerformanceFeeData()",
|
|
@@ -576,6 +594,16 @@ class PlasmaVault(ContractWrapper):
|
|
|
576
594
|
for market_id, fuse in zip(market_ids, fuses, strict=True)
|
|
577
595
|
]
|
|
578
596
|
|
|
597
|
+
def read_stored_markets_total(self, ctx: Web3Context) -> Amount:
|
|
598
|
+
"""The global market total the vault stores (the
|
|
599
|
+
``totalAssetsInAllMarkets`` term of ``totalAssets``), read from its
|
|
600
|
+
ERC-7201 slot; vaults built before June 2024 keep it at an earlier slot,
|
|
601
|
+
read when the current one is empty."""
|
|
602
|
+
for slot in (_TOTAL_ASSETS_IN_ALL_MARKETS_SLOT, _LEGACY_TOTAL_ASSETS_SLOT):
|
|
603
|
+
if value := int.from_bytes(ctx.get_storage_at(self._address, slot)):
|
|
604
|
+
return Amount(value)
|
|
605
|
+
return Amount(0)
|
|
606
|
+
|
|
579
607
|
def _address_at(self, ctx: Web3Context, slot: int) -> ChecksumAddress:
|
|
580
608
|
return _address_in_word(ctx.get_storage_at(self._address, slot))
|
|
581
609
|
|
|
@@ -397,6 +397,16 @@ class ERC20Entry(_Base):
|
|
|
397
397
|
class Reconciliation(_Base):
|
|
398
398
|
balance_fuses_total: Amount
|
|
399
399
|
underlying_on_vault: Amount
|
|
400
|
+
rewards_manager_balance: Amount | None = Field(
|
|
401
|
+
default=None,
|
|
402
|
+
description="RewardsClaimManager.balanceOf(), which totalAssets includes; "
|
|
403
|
+
"0 without a manager or when it could not be read.",
|
|
404
|
+
)
|
|
405
|
+
management_fee_netted: Amount | None = Field(
|
|
406
|
+
default=None,
|
|
407
|
+
description="Unrealized management fee subtracted from sum: totalAssets "
|
|
408
|
+
"nets it on vaults built before ipor-fusion #475; 0 otherwise.",
|
|
409
|
+
)
|
|
400
410
|
erc20_direct_total: Amount
|
|
401
411
|
sum: Amount
|
|
402
412
|
on_chain_total_assets: Amount
|
|
@@ -764,10 +774,19 @@ class FindingModel(_Base):
|
|
|
764
774
|
|
|
765
775
|
|
|
766
776
|
class ReconciliationModel(_Base):
|
|
767
|
-
"""totalAssets against balance fuses
|
|
777
|
+
"""totalAssets against balance fuses, idle underlying and the rewards
|
|
778
|
+
claim manager's balance."""
|
|
768
779
|
|
|
769
780
|
balance_fuses_total: Amount
|
|
770
781
|
underlying_on_vault: Amount
|
|
782
|
+
rewards_manager_balance: Amount = Field(
|
|
783
|
+
description="RewardsClaimManager.balanceOf(), which totalAssets includes; "
|
|
784
|
+
"0 without a manager or when it could not be read."
|
|
785
|
+
)
|
|
786
|
+
management_fee_netted: Amount = Field(
|
|
787
|
+
description="Unrealized management fee subtracted from sum: totalAssets "
|
|
788
|
+
"nets it on vaults built before ipor-fusion #475; 0 otherwise."
|
|
789
|
+
)
|
|
771
790
|
erc20_direct_total: Amount = Field(
|
|
772
791
|
description="Informational: already valued by the ERC20_VAULT_BALANCE "
|
|
773
792
|
"balance fuse, never added to sum."
|
|
@@ -797,6 +816,12 @@ class ReconciliationModel(_Base):
|
|
|
797
816
|
underlying_on_vault=amount(
|
|
798
817
|
rec.underlying_on_vault, rec.underlying_on_vault_usd
|
|
799
818
|
),
|
|
819
|
+
rewards_manager_balance=amount(
|
|
820
|
+
rec.rewards_manager_balance, rec.rewards_manager_balance_usd
|
|
821
|
+
),
|
|
822
|
+
management_fee_netted=amount(
|
|
823
|
+
rec.management_fee_netted, rec.management_fee_netted_usd
|
|
824
|
+
),
|
|
800
825
|
erc20_direct_total=amount(
|
|
801
826
|
rec.erc20_direct_total, rec.erc20_direct_total_usd
|
|
802
827
|
),
|
{ipor_fusion-4.0.0.dev191 → ipor_fusion-4.0.0.dev200}/src/ipor_fusion/readers/lending_health.py
RENAMED
|
@@ -7,7 +7,6 @@ for lending positions held by Plasma Vaults.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
-
import math
|
|
11
10
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
11
|
from dataclasses import dataclass
|
|
13
12
|
|
|
@@ -24,6 +23,7 @@ from ipor_fusion.readers.aave_v4 import AaveV4OracleReader, AaveV4SpokeReader
|
|
|
24
23
|
from ipor_fusion.readers.morpho import (
|
|
25
24
|
MorphoReader,
|
|
26
25
|
morpho_blue_address,
|
|
26
|
+
to_assets_up,
|
|
27
27
|
)
|
|
28
28
|
from ipor_fusion.substrates import decode_substrate, market_name
|
|
29
29
|
from ipor_fusion.types import (
|
|
@@ -145,13 +145,6 @@ def _call_morpho_oracle_price(ctx: Web3Context, oracle_address: ChecksumAddress)
|
|
|
145
145
|
return price
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def _shares_to_assets_up(shares: int, total_assets: int, total_shares: int) -> int:
|
|
149
|
-
"""Convert shares to assets, rounding up (Morpho convention)."""
|
|
150
|
-
if total_shares == 0:
|
|
151
|
-
return shares
|
|
152
|
-
return math.ceil(shares * (total_assets + 1) / (total_shares + 1))
|
|
153
|
-
|
|
154
|
-
|
|
155
148
|
def _compute_morpho_market_health(
|
|
156
149
|
ctx: Web3Context,
|
|
157
150
|
reader: MorphoReader,
|
|
@@ -197,7 +190,7 @@ def _compute_morpho_market_health(
|
|
|
197
190
|
_logger.debug("Failed to read Morpho oracle price for %s", morpho_market_id)
|
|
198
191
|
return None
|
|
199
192
|
|
|
200
|
-
borrowed =
|
|
193
|
+
borrowed = to_assets_up(
|
|
201
194
|
position.borrow_shares, market.total_borrow_assets, market.total_borrow_shares
|
|
202
195
|
)
|
|
203
196
|
|
|
@@ -62,6 +62,26 @@ def morpho_blue_address(chain_id: ChainId) -> ChecksumAddress:
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
# Morpho Blue's SharesMathLib: virtual shares and assets offset every
|
|
66
|
+
# conversion, so a market's first depositor cannot inflate the share price.
|
|
67
|
+
VIRTUAL_SHARES = 10**6
|
|
68
|
+
VIRTUAL_ASSETS = 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def to_assets_down(shares: int, total_assets: int, total_shares: int) -> Amount:
|
|
72
|
+
"""``SharesMathLib.toAssetsDown``: supply shares to assets, rounded down."""
|
|
73
|
+
return Amount(
|
|
74
|
+
shares * (total_assets + VIRTUAL_ASSETS) // (total_shares + VIRTUAL_SHARES)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_assets_up(shares: int, total_assets: int, total_shares: int) -> Amount:
|
|
79
|
+
"""``SharesMathLib.toAssetsUp``: borrow shares to assets, rounded up."""
|
|
80
|
+
numerator = shares * (total_assets + VIRTUAL_ASSETS)
|
|
81
|
+
denominator = total_shares + VIRTUAL_SHARES
|
|
82
|
+
return Amount((numerator + denominator - 1) // denominator)
|
|
83
|
+
|
|
84
|
+
|
|
65
85
|
@dataclass(slots=True)
|
|
66
86
|
class MorphoMarket:
|
|
67
87
|
"""On-chain state of a Morpho Blue lending market."""
|
|
@@ -206,22 +226,12 @@ class MorphoReader(ContractWrapper):
|
|
|
206
226
|
params_read = batch.add(self.market_params(market_id))
|
|
207
227
|
batch.run()
|
|
208
228
|
pos, market, params = pos_read.value, market_read.value, params_read.value
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
else:
|
|
216
|
-
borrow_assets = 0
|
|
217
|
-
if market.total_supply_shares > 0:
|
|
218
|
-
supply_assets = (
|
|
219
|
-
pos.supply_shares
|
|
220
|
-
* market.total_supply_assets
|
|
221
|
-
// market.total_supply_shares
|
|
222
|
-
)
|
|
223
|
-
else:
|
|
224
|
-
supply_assets = 0
|
|
229
|
+
borrow_assets = to_assets_up(
|
|
230
|
+
pos.borrow_shares, market.total_borrow_assets, market.total_borrow_shares
|
|
231
|
+
)
|
|
232
|
+
supply_assets = to_assets_down(
|
|
233
|
+
pos.supply_shares, market.total_supply_assets, market.total_supply_shares
|
|
234
|
+
)
|
|
225
235
|
return MorphoPositionBreakdown(
|
|
226
236
|
market_id=market_id,
|
|
227
237
|
loan_token=params.loan_token,
|
|
@@ -79,6 +79,7 @@ Severity: TypeAlias = Literal["ok", "warning", "critical"]
|
|
|
79
79
|
Verdict: TypeAlias = Literal["ok", "warning", "critical"]
|
|
80
80
|
Check: TypeAlias = Literal[
|
|
81
81
|
"unimplemented_getter",
|
|
82
|
+
"rewards_manager_balance",
|
|
82
83
|
"orphan_fuse",
|
|
83
84
|
"missing_erc20_dependency",
|
|
84
85
|
"unpriceable_token",
|
|
@@ -119,6 +119,22 @@ class VaultState(Protocol):
|
|
|
119
119
|
def total_assets(self) -> Amount: ...
|
|
120
120
|
@property
|
|
121
121
|
def underlying_balance_on_vault(self) -> Amount: ...
|
|
122
|
+
@property
|
|
123
|
+
def stored_markets_total(self) -> Amount | None:
|
|
124
|
+
"""The global market total the vault stores; None when not read."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def unrealized_management_fee(self) -> Amount | None:
|
|
129
|
+
"""``getUnrealizedManagementFee()``, None when not read."""
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def rewards_manager_balance(self) -> Amount | None:
|
|
134
|
+
"""The RewardsClaimManager's balanceOf(), part of totalAssets; 0
|
|
135
|
+
without a manager, None when it could not be read."""
|
|
136
|
+
...
|
|
137
|
+
|
|
122
138
|
@property
|
|
123
139
|
def price_oracle(self) -> ChecksumAddress: ...
|
|
124
140
|
@property
|
|
@@ -155,20 +171,30 @@ class Finding:
|
|
|
155
171
|
|
|
156
172
|
@dataclass(frozen=True)
|
|
157
173
|
class Reconciliation:
|
|
158
|
-
"""``totalAssets`` against what the vault's markets
|
|
159
|
-
up to. Raw amounts are in the underlying asset's decimals; ``*_usd`` values
|
|
174
|
+
"""``totalAssets`` against what the vault's markets, idle underlying and
|
|
175
|
+
rewards claim manager add up to. Raw amounts are in the underlying asset's decimals; ``*_usd`` values
|
|
160
176
|
are 0 when the asset has no USD price (``on_chain_total_assets_usd``:
|
|
161
177
|
``None``).
|
|
162
178
|
|
|
163
179
|
Balance fuses already value non-underlying ERC20s (via
|
|
164
180
|
ERC20_VAULT_BALANCE), so ``sum`` is balance fuses plus idle underlying
|
|
165
|
-
|
|
181
|
+
plus the rewards claim manager's balance, minus the unrealized management
|
|
182
|
+
fee where ``totalAssets`` nets it (``management_fee_netted``);
|
|
183
|
+
``erc20_direct_total`` is informational and never added to it.
|
|
166
184
|
"""
|
|
167
185
|
|
|
168
186
|
balance_fuses_total: Amount
|
|
169
187
|
balance_fuses_total_usd: float
|
|
170
188
|
underlying_on_vault: Amount
|
|
171
189
|
underlying_on_vault_usd: float
|
|
190
|
+
# RewardsClaimManager.balanceOf(): vested rewards totalAssets counts
|
|
191
|
+
# (0 without a manager or when it could not be read).
|
|
192
|
+
rewards_manager_balance: Amount
|
|
193
|
+
rewards_manager_balance_usd: float
|
|
194
|
+
# The unrealized management fee totalAssets subtracts, on vaults whose
|
|
195
|
+
# contracts net it (before ipor-fusion #475); 0 on a vault returning gross.
|
|
196
|
+
management_fee_netted: Amount
|
|
197
|
+
management_fee_netted_usd: float
|
|
172
198
|
erc20_direct_total: Amount
|
|
173
199
|
erc20_direct_total_usd: float
|
|
174
200
|
sum: Amount
|
|
@@ -181,7 +207,8 @@ class Reconciliation:
|
|
|
181
207
|
# sharesToRelease of the WithdrawManager converted to assets.
|
|
182
208
|
pending_withdrawals: Amount
|
|
183
209
|
pending_withdrawals_usd: float
|
|
184
|
-
# totalAssets minus idle underlying
|
|
210
|
+
# totalAssets minus idle underlying and the rewards claim manager's
|
|
211
|
+
# balance: the global market total the vault
|
|
185
212
|
# stores, which per-market caches drift from after partial refreshes.
|
|
186
213
|
implied_market_total: Amount
|
|
187
214
|
|
|
@@ -259,6 +286,7 @@ def validate_vault_state(state: VaultState) -> VaultValidation:
|
|
|
259
286
|
reconciliation = _reconcile(state)
|
|
260
287
|
findings = [
|
|
261
288
|
*_unimplemented_getter_findings(state),
|
|
289
|
+
*_rewards_manager_findings(state),
|
|
262
290
|
*_orphan_fuse_findings(state),
|
|
263
291
|
*_missing_erc20_dependency_findings(state),
|
|
264
292
|
*_unpriceable_token_findings(state),
|
|
@@ -300,9 +328,15 @@ def _reconcile(state: VaultState) -> Reconciliation:
|
|
|
300
328
|
if underlying > 0 and price:
|
|
301
329
|
underlying_usd = (underlying / 10**decimals) * price
|
|
302
330
|
|
|
303
|
-
|
|
304
|
-
|
|
331
|
+
rewards = state.rewards_manager_balance or Amount(0)
|
|
332
|
+
rewards_usd = _usd(rewards, decimals, price)
|
|
333
|
+
|
|
305
334
|
on_chain = state.total_assets
|
|
335
|
+
gross = Amount(balance_fuses_total + underlying + rewards)
|
|
336
|
+
fee = _netted_management_fee(state, underlying + rewards)
|
|
337
|
+
fee_usd = _usd(fee, decimals, price)
|
|
338
|
+
total = Amount(gross - fee)
|
|
339
|
+
total_usd = balance_fuses_usd + underlying_usd + rewards_usd - fee_usd
|
|
306
340
|
delta = Amount(total - on_chain)
|
|
307
341
|
delta_usd = abs(total_usd - (on_chain / 10**decimals * price)) if price else 0
|
|
308
342
|
delta_percent = abs(delta / on_chain * 100) if on_chain else 0.0
|
|
@@ -325,6 +359,10 @@ def _reconcile(state: VaultState) -> Reconciliation:
|
|
|
325
359
|
balance_fuses_total_usd=balance_fuses_usd,
|
|
326
360
|
underlying_on_vault=underlying,
|
|
327
361
|
underlying_on_vault_usd=underlying_usd,
|
|
362
|
+
rewards_manager_balance=rewards,
|
|
363
|
+
rewards_manager_balance_usd=rewards_usd,
|
|
364
|
+
management_fee_netted=fee,
|
|
365
|
+
management_fee_netted_usd=fee_usd,
|
|
328
366
|
erc20_direct_total=erc20.value_in_asset,
|
|
329
367
|
erc20_direct_total_usd=erc20.value_usd,
|
|
330
368
|
sum=total,
|
|
@@ -336,10 +374,36 @@ def _reconcile(state: VaultState) -> Reconciliation:
|
|
|
336
374
|
delta_percent=delta_percent,
|
|
337
375
|
pending_withdrawals=pending,
|
|
338
376
|
pending_withdrawals_usd=pending_usd,
|
|
339
|
-
implied_market_total=Amount(max(0, on_chain - underlying)),
|
|
377
|
+
implied_market_total=Amount(max(0, on_chain + fee - underlying - rewards)),
|
|
340
378
|
)
|
|
341
379
|
|
|
342
380
|
|
|
381
|
+
def _usd(amount: int, decimals: int, price: float | None) -> float:
|
|
382
|
+
return (amount / 10**decimals) * price if amount and price else 0.0
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _netted_management_fee(state: VaultState, outside_markets: int) -> Amount:
|
|
386
|
+
"""The unrealized management fee ``totalAssets`` subtracts, 0 when it
|
|
387
|
+
returns gross.
|
|
388
|
+
|
|
389
|
+
PlasmaVault contracts before ipor-fusion #475 (Feb 2026) return
|
|
390
|
+
``max(0, gross - fee)``, later ones ``gross``, where gross is idle
|
|
391
|
+
underlying plus the stored global market total plus the rewards manager's
|
|
392
|
+
balance. The deployed version is not readable, so the formula is decided by
|
|
393
|
+
which one ``totalAssets`` matches, from the stored global total rather than
|
|
394
|
+
the per-market caches the reconciliation compares (a drift between those
|
|
395
|
+
must not pass for the fee). Without the stored total, the fee or a match,
|
|
396
|
+
nothing is netted.
|
|
397
|
+
"""
|
|
398
|
+
fee, stored = state.unrealized_management_fee, state.stored_markets_total
|
|
399
|
+
if not fee or stored is None:
|
|
400
|
+
return Amount(0)
|
|
401
|
+
gross = outside_markets + stored
|
|
402
|
+
if state.total_assets == gross or state.total_assets != max(0, gross - fee):
|
|
403
|
+
return Amount(0)
|
|
404
|
+
return Amount(min(fee, gross))
|
|
405
|
+
|
|
406
|
+
|
|
343
407
|
def _unimplemented_getter_findings(state: VaultState) -> list[Finding]:
|
|
344
408
|
return [
|
|
345
409
|
Finding(
|
|
@@ -351,6 +415,20 @@ def _unimplemented_getter_findings(state: VaultState) -> list[Finding]:
|
|
|
351
415
|
]
|
|
352
416
|
|
|
353
417
|
|
|
418
|
+
def _rewards_manager_findings(state: VaultState) -> list[Finding]:
|
|
419
|
+
if state.rewards_manager_balance is not None:
|
|
420
|
+
return []
|
|
421
|
+
return [
|
|
422
|
+
Finding(
|
|
423
|
+
"rewards_manager_balance",
|
|
424
|
+
"warning",
|
|
425
|
+
"WARNING — the rewards claim manager or its balanceOf() could not be "
|
|
426
|
+
"read: totalAssets includes that balance, so the reconciliation "
|
|
427
|
+
"leaves it out and its delta may be that balance",
|
|
428
|
+
)
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
|
|
354
432
|
def _orphan_fuse_findings(state: VaultState) -> list[Finding]:
|
|
355
433
|
"""Action fuses whose market has no balance fuse: their positions silently
|
|
356
434
|
bypass ``totalAssets``. Flow-through markets (flash loans, swaps) are
|
|
@@ -522,8 +600,13 @@ def _reconciliation_findings(
|
|
|
522
600
|
symbol = state.asset_symbol
|
|
523
601
|
expected = reconciliation.sum
|
|
524
602
|
percent = abs(expected - total_assets) / total_assets * 100
|
|
603
|
+
terms = "Balance fuses + underlying"
|
|
604
|
+
if reconciliation.rewards_manager_balance > 0:
|
|
605
|
+
terms += " + rewards manager"
|
|
606
|
+
if reconciliation.management_fee_netted > 0:
|
|
607
|
+
terms += " - unrealized management fee"
|
|
525
608
|
line = (
|
|
526
|
-
f"
|
|
609
|
+
f"{terms} = {_format_amount(expected, decimals)} "
|
|
527
610
|
f"{symbol} vs totalAssets {_format_amount(total_assets, decimals)} "
|
|
528
611
|
f"{symbol} ({percent:.2f}%)"
|
|
529
612
|
)
|
|
@@ -541,7 +624,9 @@ def _reconciliation_findings(
|
|
|
541
624
|
)
|
|
542
625
|
)
|
|
543
626
|
divergence = reconciliation.market_storage_divergence
|
|
544
|
-
|
|
627
|
+
# With the rewards manager's balance unknown the divergence may be that
|
|
628
|
+
# balance; the rewards_manager_balance warning says so instead.
|
|
629
|
+
if abs(divergence) > 10**decimals and state.rewards_manager_balance is not None:
|
|
545
630
|
findings.append(
|
|
546
631
|
Finding(
|
|
547
632
|
"market_storage_drift",
|
|
@@ -26,9 +26,10 @@ from ipor_fusion.core.access import AccessManager, RoleAccount, role_account_sor
|
|
|
26
26
|
from ipor_fusion.core.context import Web3Context
|
|
27
27
|
from ipor_fusion.core.contract import Call
|
|
28
28
|
from ipor_fusion.core.erc20 import ERC20
|
|
29
|
-
from ipor_fusion.core.multicall import Multicall3
|
|
29
|
+
from ipor_fusion.core.multicall import BatchValue, Multicall3
|
|
30
30
|
from ipor_fusion.core.oracle import PriceOracleMiddleware, price_oracle_middleware
|
|
31
31
|
from ipor_fusion.core.plasma_vault import BalanceFuse, PlasmaVault
|
|
32
|
+
from ipor_fusion.core.rewards_manager import RewardsManager
|
|
32
33
|
from ipor_fusion.errors import (
|
|
33
34
|
EmptyCallResultError,
|
|
34
35
|
UnsupportedVaultVersionError,
|
|
@@ -143,6 +144,13 @@ class VaultData:
|
|
|
143
144
|
# ERC20_VAULT_BALANCE substrate, so it is read directly in
|
|
144
145
|
# fetch_vault_data rather than derived from substrates.
|
|
145
146
|
underlying_balance_on_vault: Amount = Amount(0)
|
|
147
|
+
# RewardsClaimManager.balanceOf() (its vested underlying), which
|
|
148
|
+
# PlasmaVault.totalAssets adds to the vault's own balance and its markets.
|
|
149
|
+
# 0 without a manager; None when the vault has one but the read failed.
|
|
150
|
+
rewards_manager_balance: Amount | None = Amount(0)
|
|
151
|
+
# The global market total the vault stores (the totalAssetsInAllMarkets
|
|
152
|
+
# term of totalAssets); None when the storage read failed.
|
|
153
|
+
stored_markets_total: Amount | None = None
|
|
146
154
|
vault_name: str = ""
|
|
147
155
|
deployment_block: int | None = None
|
|
148
156
|
deployment_timestamp: Timestamp | None = None
|
|
@@ -204,6 +212,11 @@ class VaultData:
|
|
|
204
212
|
token_symbols: dict[HexAddress, str] = field(default_factory=dict)
|
|
205
213
|
token_decimals: dict[HexAddress, Decimals | None] = field(default_factory=dict)
|
|
206
214
|
|
|
215
|
+
@property
|
|
216
|
+
def unrealized_management_fee(self) -> Amount | None:
|
|
217
|
+
"""``getUnrealizedManagementFee()`` from the fee snapshot."""
|
|
218
|
+
return self.fee_data.unrealized_management_fee if self.fee_data else None
|
|
219
|
+
|
|
207
220
|
|
|
208
221
|
_MARKET_ID_SELECTOR = function_signature_to_4byte_selector("MARKET_ID()")
|
|
209
222
|
|
|
@@ -342,6 +355,7 @@ class _VaultReads:
|
|
|
342
355
|
asset_price_usd: float | None
|
|
343
356
|
fuse_markets: dict[FuseAddress, MarketId] | None
|
|
344
357
|
unimplemented_getters: list[str]
|
|
358
|
+
rewards_manager_balance: Amount | None
|
|
345
359
|
|
|
346
360
|
|
|
347
361
|
def _fetch_vault_reads(
|
|
@@ -351,8 +365,9 @@ def _fetch_vault_reads(
|
|
|
351
365
|
oracle wrapper resolved for them (reused, so its ABI is probed once).
|
|
352
366
|
|
|
353
367
|
Two Multicall3 round trips: vault getters, then everything that needs the
|
|
354
|
-
asset, the oracle
|
|
355
|
-
each fuse's MARKET_ID()
|
|
368
|
+
asset, the oracle, the fuse list or the rewards claim manager (asset
|
|
369
|
+
metadata, idle balance, price, each fuse's MARKET_ID(), the manager's
|
|
370
|
+
vested balance).
|
|
356
371
|
"""
|
|
357
372
|
pv = plasma_vault
|
|
358
373
|
batch = Multicall3(ctx).batch()
|
|
@@ -365,6 +380,7 @@ def _fetch_vault_reads(
|
|
|
365
380
|
instant_fuses = batch.add(pv.get_instant_withdrawal_fuses())
|
|
366
381
|
vault_name = batch.try_add(pv.name())
|
|
367
382
|
rewards_manager = batch.try_add(pv.get_rewards_claim_manager_address())
|
|
383
|
+
legacy_rewards_manager = batch.try_add(pv.get_rewards_manager_address())
|
|
368
384
|
supply_cap_read = batch.try_add(pv.get_total_supply_cap())
|
|
369
385
|
middleware_oracle = batch.try_add(pv.get_price_oracle_middleware_address())
|
|
370
386
|
legacy_oracle = batch.try_add(pv.get_price_oracle_address())
|
|
@@ -398,6 +414,18 @@ def _fetch_vault_reads(
|
|
|
398
414
|
fuse_market_ids = [
|
|
399
415
|
(fuse, batch.try_add(_fuse_market_id_call(fuse))) for fuse in fuses
|
|
400
416
|
]
|
|
417
|
+
# Vaults before the RewardsClaimManager rename answer the legacy getter;
|
|
418
|
+
# None from both leaves the manager, and so its balance, unknown.
|
|
419
|
+
manager = (
|
|
420
|
+
rewards_manager.value
|
|
421
|
+
if rewards_manager.value is not None
|
|
422
|
+
else legacy_rewards_manager.value
|
|
423
|
+
)
|
|
424
|
+
manager_balance = (
|
|
425
|
+
batch.try_add(RewardsManager(manager).balance_of())
|
|
426
|
+
if manager is not None and int(manager, 16) != 0
|
|
427
|
+
else None
|
|
428
|
+
)
|
|
401
429
|
batch.run()
|
|
402
430
|
# Fuses without a readable MARKET_ID() are left out; the rest detect orphan
|
|
403
431
|
# markets (action fuse registered, no balance fuse for its market).
|
|
@@ -416,7 +444,8 @@ def _fetch_vault_reads(
|
|
|
416
444
|
fuses=fuses,
|
|
417
445
|
instant_fuses=instant_fuses.value,
|
|
418
446
|
vault_name=vault_name.value or "",
|
|
419
|
-
rewards_manager=
|
|
447
|
+
rewards_manager=manager,
|
|
448
|
+
rewards_manager_balance=_rewards_manager_balance(manager, manager_balance),
|
|
420
449
|
asset_decimals=asset_decimals.value,
|
|
421
450
|
underlying_balance_on_vault=underlying_balance.value,
|
|
422
451
|
asset_symbol=symbol.value or "?",
|
|
@@ -426,6 +455,16 @@ def _fetch_vault_reads(
|
|
|
426
455
|
), oracle
|
|
427
456
|
|
|
428
457
|
|
|
458
|
+
def _rewards_manager_balance(
|
|
459
|
+
manager: ChecksumAddress | None, read: BatchValue[Amount | None] | None
|
|
460
|
+
) -> Amount | None:
|
|
461
|
+
"""The manager's balance; 0 for the zero address (no manager), None when
|
|
462
|
+
the manager or its balance could not be read."""
|
|
463
|
+
if read is not None:
|
|
464
|
+
return read.value
|
|
465
|
+
return None if manager is None else Amount(0)
|
|
466
|
+
|
|
467
|
+
|
|
429
468
|
def _fetch_role_accounts(
|
|
430
469
|
ctx: Web3Context, plasma_vault: PlasmaVault
|
|
431
470
|
) -> list[RoleAccount] | None:
|
|
@@ -587,6 +626,9 @@ def fetch_vault_data(
|
|
|
587
626
|
f_fees = pool.submit(_fetch_fee_data, ctx, plasma_vault)
|
|
588
627
|
f_withdraw = pool.submit(_fetch_withdraw_manager, ctx, plasma_vault)
|
|
589
628
|
f_roles = pool.submit(_fetch_role_accounts, ctx, plasma_vault)
|
|
629
|
+
f_stored_total = pool.submit(
|
|
630
|
+
_safe_call, partial(plasma_vault.read_stored_markets_total, ctx)
|
|
631
|
+
)
|
|
590
632
|
f_markets = pool.submit(
|
|
591
633
|
_fetch_market_reads, ctx, plasma_vault, with_aave_pools=lending
|
|
592
634
|
)
|
|
@@ -622,6 +664,7 @@ def fetch_vault_data(
|
|
|
622
664
|
fee_data=f_fees.result(),
|
|
623
665
|
role_accounts=f_roles.result(),
|
|
624
666
|
hypercore=f_hypercore.result(),
|
|
667
|
+
stored_markets_total=f_stored_total.result(),
|
|
625
668
|
)
|
|
626
669
|
|
|
627
670
|
|
|
@@ -645,6 +688,7 @@ def _snapshot(
|
|
|
645
688
|
fee_data: FeeData | None,
|
|
646
689
|
role_accounts: list[RoleAccount] | None,
|
|
647
690
|
hypercore: HyperCoreVaultState | None,
|
|
691
|
+
stored_markets_total: Amount | None,
|
|
648
692
|
) -> VaultData:
|
|
649
693
|
"""`VaultData` from the pipelines' results."""
|
|
650
694
|
return VaultData(
|
|
@@ -664,6 +708,8 @@ def _snapshot(
|
|
|
664
708
|
instant_fuses=reads.instant_fuses,
|
|
665
709
|
vault_name=reads.vault_name,
|
|
666
710
|
rewards_manager=reads.rewards_manager,
|
|
711
|
+
rewards_manager_balance=reads.rewards_manager_balance,
|
|
712
|
+
stored_markets_total=stored_markets_total,
|
|
667
713
|
asset_decimals=reads.asset_decimals,
|
|
668
714
|
underlying_balance_on_vault=reads.underlying_balance_on_vault,
|
|
669
715
|
asset_symbol=reads.asset_symbol,
|
|
@@ -305,6 +305,8 @@ class Erc20BalanceJson(TypedDict):
|
|
|
305
305
|
class ReconciliationJson(TypedDict):
|
|
306
306
|
balance_fuses_total: UsdAmountJson
|
|
307
307
|
underlying_on_vault: UsdAmountJson
|
|
308
|
+
rewards_manager_balance: UsdAmountJson
|
|
309
|
+
management_fee_netted: UsdAmountJson
|
|
308
310
|
erc20_direct_total: UsdAmountJson
|
|
309
311
|
sum: UsdAmountJson
|
|
310
312
|
on_chain_total_assets: UsdAmountJson
|
|
@@ -810,6 +812,14 @@ def _reconciliation_json(recon: Reconciliation, decimals: int) -> Reconciliation
|
|
|
810
812
|
"underlying_on_vault": _amount_json(
|
|
811
813
|
recon.underlying_on_vault, decimals, recon.underlying_on_vault_usd
|
|
812
814
|
),
|
|
815
|
+
"rewards_manager_balance": _amount_json(
|
|
816
|
+
recon.rewards_manager_balance,
|
|
817
|
+
decimals,
|
|
818
|
+
recon.rewards_manager_balance_usd,
|
|
819
|
+
),
|
|
820
|
+
"management_fee_netted": _amount_json(
|
|
821
|
+
recon.management_fee_netted, decimals, recon.management_fee_netted_usd
|
|
822
|
+
),
|
|
813
823
|
"erc20_direct_total": _amount_json(
|
|
814
824
|
recon.erc20_direct_total, decimals, recon.erc20_direct_total_usd
|
|
815
825
|
),
|