diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40314753..022d684a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,28 +4,86 @@ on: pull_request: branches: ['*'] +permissions: + contents: read + +env: + NODE_VERSION: '22.16.0' + PYTHON_VERSION: '3.11.11' + jobs: build_and_test_smart_contracts: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - - uses: actions/setup-python@v2 - - name: "Install SmartPy" - run: | - bash <(curl -s https://legacy.smartpy.io/cli/install.sh) --yes + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: "Install verified SmartPy 0.16.0" + run: tools/install-smartpy.sh + - name: "Audit SmartPy runtime dependencies" + run: npm audit --omit=dev --prefix tools/smartpy - name: "Version Check" run: | - ~/smartpy-cli/SmartPy.sh --version + test "$(~/smartpy-cli/SmartPy.sh --version)" = "SmartPy Version: 0.16.0" - name: "Run tests" run: | bash contracts/tests/run_tests.sh ~/smartpy-cli/SmartPy.sh + - name: "Check per-market IRM wiring" + run: | + python3 deploy/compile_targets/tests/test_irm_wiring.py + - name: "Install deploy script dependencies (needed by test_deploy_pipeline_wiring.py, which requires deploy_script/util.js, and by test_operation_size.py, which forges originations with @taquito/local-forging)" + working-directory: deploy/deploy_script + run: | + npm ci + - name: "Check deploy pipeline wiring (compile targets, IRM config source, manifest path parity)" + run: | + python3 deploy/compile_targets/tests/test_deploy_pipeline_wiring.py + - name: "Check mainnet governance payload" + run: | + python3 deploy/compile_targets/tests/test_mainnet_governance_payload.py + - name: "Check contract origination sizes stay within safety threshold" + run: | + python3 deploy/compile_targets/tests/test_operation_size.py ~/smartpy-cli/SmartPy.sh + - name: "Verify reproducible contract compilation" + env: + COMPILED_HASHES_OUTPUT: compiled-contract-hashes.json + run: | + python3 deploy/compile_targets/tests/test_reproducible_build.py ~/smartpy-cli/SmartPy.sh + - name: "Publish compiled contract hashes" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: compiled-contract-hashes + path: compiled-contract-hashes.json + if-no-files-found: error + retention-days: 30 + + build_and_test_deploy_scripts: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: "Install deploy script dependencies" + working-directory: deploy/deploy_script + run: | + npm ci + - name: "Run deploy script guard unit tests (offline, no network access)" + working-directory: deploy/deploy_script + run: | + npm test build_and_test_typescript: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} - name: "Build and test utility package" working-directory: src/util run: | diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..5b540673 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.16.0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..5b540673 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.16.0 diff --git a/README.md b/README.md index ac7e66d0..cf9af9c5 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,29 @@ For the detailed description please refer to the [wiki pages](https://github.com ## Project Structure -SmartPy CLI is required to interact with contracts. For installation please refer to https://smartpy.io/cli/ +SmartPy Legacy CLI **0.16.0** is the only supported compiler version for this +codebase. The contracts use the legacy SmartPy syntax and cannot be compiled with +the current SmartPy language without a reviewed migration. -Install legacy version: `bash <(curl -s https://legacy.smartpy.io/cli/install.sh) --yes` +Install the pinned compiler and runtime dependencies with: + +```sh +tools/install-smartpy.sh +``` + +The installer fails closed unless the downloaded archive matches the repository's +SHA-256 pin, installs npm dependencies with `npm ci` from +`tools/smartpy/package-lock.json`, and verifies that the compiler reports exactly +`SmartPy Version: 0.16.0`. Do not use the upstream `curl | bash` installer for +release builds. + +CI additionally pins Ubuntu 22.04, Node.js 22.16.0, Python 3.11.11, and GitHub +Actions to immutable commit SHAs. + +Node.js 22.16.0 is also the required local and production deployment runtime. +Use `nvm use` (or a version manager that reads `.node-version`) before running +`npm ci`; the deployment package rejects other Node.js versions because Taquito +25 requires Node.js 22 or newer. Code is organized in the following structure @@ -72,6 +92,113 @@ cd TezFin ./contracts/tests/run_tests.sh ~/smartpy-cli/SmartPy.sh ``` +The contract suite includes `CapPostStateTest.py`, which exercises supply and +borrow caps through real ꜰToken `mint` and `borrow` entrypoints at below-cap, +exact-cap, and cap-plus-one boundaries, including exchange-rate rounding. + +## Required Deployment Tests + +These checks guard the deployment pipeline itself (not the contracts' business logic) +and all run offline/in CI without needing a live Tezos node: + +- **Per-market IRM wiring** (`deploy/compile_targets/tests/test_irm_wiring.py`) - static + check that each ꜰToken market compile target (`CompileCUSDt.py`, `CompileCUSDtz.py`, + `CompileCXTZ.py`, `CompileTzBTC.py`) references its own, asset-specific + `_IRM` config key instead of accidentally reusing another market's IRM. + ```sh + python3 deploy/compile_targets/tests/test_irm_wiring.py + ``` +- **Deploy pipeline wiring** (`deploy/compile_targets/tests/test_deploy_pipeline_wiring.py`) + - three cheap, static (no SmartPy, no network) checks in one script: + - every `Compile*.py` referenced from `deploy_previewnet.sh`/`deploy_mainnet.sh` + actually exists on disk (catches a typo'd/renamed/deleted compile target left + dangling in a shell script); + - `CompileCtzBTC_IRM.py` reads its parameters only from `CFG.CtzBTC_IRM` (its own, + asset-specific IRM config block), not another market's by mistake; + - `Config.py` (Python) and `util.js`'s `resolveDeployResultPath()` (JS) actually + resolve to the same default manifest file for a given `networkProfile` + ("previewnet"/"mainnet"/unset) - executed for real (each language's actual source, + not a re-implementation) rather than just asserted in a comment. + ```sh + python3 deploy/compile_targets/tests/test_deploy_pipeline_wiring.py + ``` +- **Mainnet governance payload** + (`deploy/compile_targets/tests/test_mainnet_governance_payload.py`) - validates + that the checked-in manifest uses an oracle max age accepted by the contract, + configures price bounds and market caps before activation, keeps unapproved + markets fail-closed, and controls mint, borrow, redeem, and liquidation + independently. + ```sh + python3 deploy/compile_targets/tests/test_mainnet_governance_payload.py + ``` +- **Contract origination size threshold** + (`deploy/compile_targets/tests/test_operation_size.py`) - performs a **fresh SmartPy + compile** (not a read of whatever is checked into `compiled_contracts/`, which can be + stale relative to the current change) of Governance, TezFinOracle, and Comptroller + into a temporary directory, using the **same SmartPy CLI flags each target is + compiled with in `deploy_previewnet.sh`/`deploy_mainnet.sh`** (e.g. + `--erase-comments --erase-var-annots --initial-cast` for Comptroller), against a + throwaway manifest of placeholder addresses (`e2e/deploy_result/deploy.json`), then + measures the **complete origination operation** for each and checks it against the + 32768-byte Tezos manager-operation limit. + + The measured quantity matters here. SmartPy's `*_sizes.csv` reports only the packed + Micheline size of the contract code and the initial storage, but the protocol's + `max_operation_data_length` applies to the whole signed operation, which also carries + the branch, source, fee/counter/gas/storage limits and a 64-byte signature - about 140 + bytes more. This check therefore forges a real origination operation offline with + `@taquito/local-forging` (see `deploy_script/measure_origination_size.js`) and gates on + that, which is the same number Taquito reports as `estimate.originate().opSize`. + Because the forging is local it needs no RPC, no funded account and no secret key, so + the authoritative figure is available on every commit rather than only at deploy time. + The code+storage numbers are still printed alongside it for continuity. + + It also warns when the remaining margin is under ~162 bytes, since Taquito batches a + reveal into the same operation group when the deployer's public key has not yet been + revealed, and that reveal shares the same 32768-byte budget. + + Requires the SmartPy CLI and `npm ci` in `deploy/deploy_script`; fails loudly + (non-zero exit) if the CLI can't be found, if the forger is unavailable, or if every + compile attempt fails, rather than silently reporting success with nothing checked. + ```sh + python3 deploy/compile_targets/tests/test_operation_size.py ~/smartpy-cli/SmartPy.sh + ``` + + To measure a single already-compiled contract directly: + ```sh + cd deploy/deploy_script + npm run measure:origination-size -- ../../TezFinBuild/compiled_contracts/Comptroller --json + ``` +- **Reproducible contract compilation** + (`deploy/compile_targets/tests/test_reproducible_build.py`) - compiles every contract + originated by `deploy_mainnet.sh` twice in clean temporary directories with the + same production flags, then compares SHA-256 hashes of canonical contract and + storage JSON. CI publishes the resulting + `compiled-contract-hashes.json` artifact for deployment and multisig review. + ```sh + COMPILED_HASHES_OUTPUT=compiled-contract-hashes.json \ + python3 deploy/compile_targets/tests/test_reproducible_build.py ~/smartpy-cli/SmartPy.sh + ``` +- **Deploy script guards** (`deploy/deploy_script/test/deploy_guards.test.js`) - unit + tests (Node's built-in test runner, no network access) for the safety checks in + `util.js`, `assert_network.js`, and `mainnet_preflight.js`: chain-id mismatch + rejection (manifest vs. connected RPC), Micheline code/storage comparison used to + decide whether an existing manifest entry can be safely reused, manifest path + resolution (`DEPLOY_MANIFEST` env var vs. per-profile default), and mainnet preflight's + required-canonical-key / vetted-address-allowlist checks. + ```sh + cd deploy/deploy_script + npm ci + npm test + ``` + +All three are wired into CI (`.github/workflows/ci.yml`). + +Not currently automated (documented here as a manual pre-mainnet step instead): a live +dry run of `deploy_mainnet.sh` against a real mainnet-like node, and confirming the +`MAINNET_CHAIN_IDS` value in `assert_network.js` against the node you actually connect +to before relying on it to reject a misconfigured network. + ## Run Contract E2E Tests To run e2e tests use the following command, you will need latest smartpy cli installed. @@ -87,19 +214,154 @@ Before listing an underlying, complete the [market listing checklist](docs/Marke To compile and deploy all contracts at once: 1. Configure parameters for contracts compilation in [Config.json](deploy/compile_targets/Config.json). Reffer to [Compilation arguments](https://github.com/RSerhii/TezFin/wiki/Compilation-arguments) -2. Configure deployment parameters in [config.json](deploy/deploy_script/config.json) +2. The deployer is configured for Tezos X Previewnet in [config.json](deploy/deploy_script/config.json). Keep only the public deployment address there; provide the matching private key from your shell (never commit it): +```sh +export TEZOS_PRIVATE_KEY='edsk...' +``` + Before deploying to a different network or with a different deployment account, update + `originator.pkh` (and `tezosNode` / `chainId`) in `config.json` to match the target. This is an + intentional manual step: the deployer will refuse to run if the signing key does not match the + configured `originator.pkh`, so committing the wrong address is a safe failure, not a silent one. + Alternatively, set `TEZOS_MNEMONIC` for a standard Tezos wallet seed phrase (and `TEZOS_MNEMONIC_PASSWORD` only if your wallet used one). The default derivation path is `44'/1729'/0'/0'`; override it with `TEZOS_DERIVATION_PATH` if needed. Legacy fundraiser accounts additionally require `TEZOS_FUNDS_EMAIL` and `TEZOS_FUNDS_PASSWORD`. The signer must match `originator.pkh` and have Previewnet XTZ. + Previewnet fee estimates can change between simulation and injection; the deployer applies a 20% fee margin. Override it only when needed with `TEZOS_FEE_SAFETY_MULTIPLIER`. 3. Install deployment dependencies ```sh cd deploy/deploy_script npm install +npm run check +npm run prepare:deploy ``` -4. Run deployment script -```sh -./deploy/shell_scripts/deploy_all_contracts.sh ~/smartpy-cli/SmartPy.sh -``` +4. Run the deployment script for the target network: + - Previewnet: + ```sh + ./deploy/shell_scripts/deploy_previewnet.sh ~/smartpy-cli/SmartPy.sh + ``` + - Mainnet (see [Mainnet Deployment](#mainnet-deployment) below before running this): + ```sh + MAINNET_DEPLOY_CONFIRM=yes ./deploy/shell_scripts/deploy_mainnet.sh ~/smartpy-cli/SmartPy.sh + ``` > To deploy a specific contract run the corresponding script in [shell_scripts](deploy/shell_scripts) +### Mainnet Deployment + +`deploy_mainnet.sh` is a separate, stricter profile from `deploy_previewnet.sh`: + +- It never runs `CompileTestData.py` — no mock tokens or mock oracle are ever compiled or + originated on this path. +- It refuses to run unless `deploy_script/config.json` declares `networkProfile: "mainnet"` **and** + the connected RPC reports a known mainnet chain id (`assert_network.js`). It also exports + `DEPLOY_MANIFEST=TezFinBuild/deploy_result/deploy.mainnet.json` at the top of the script (unless + already set) so every step — the plan/preflight check, `prepare.js`, `deploy.js`, and the SmartPy + compile targets — reads and writes the exact same manifest file. +- Before touching the manifest at all, it runs `mainnet_preflight.js`, which requires the manifest to + already contain vetted, on-chain-verified canonical addresses for `PriceOracle`, `USDt`, and `tzBTC` + (checked both against an on-chain existence check and a required hardcoded allowlist in + `mainnet_preflight.js`; a missing allowlist entry fails deployment), prints the full deployment plan (network, chain id, manifest, + canonical inputs, and any addresses already recorded in the manifest), and requires + `MAINNET_DEPLOY_CONFIRM=yes` to proceed past that point. Only after this passes does `prepare.js` run + and write `OriginatorAddress` to the manifest — declining confirmation leaves the manifest untouched. +- Before every mainnet origination, the programmatic preflight in `util.js` also runs + `verify_mainnet_oracle.js`. This applies to both the shell script and raw `node deploy.js` usage and + rejects missing views, zero prices, stale or future timestamps, and millisecond timestamps. +- After origination it reminds you to complete the [Post-Deployment Admin + Handoff](#post-deployment-admin-handoff-mainnet) before unpausing any market. + +> `USDtz` is intentionally not required by `mainnet_preflight.js`: no compile target in either deploy +> script currently originates a `CUSDtz` market. If a `CUSDtz` market is added to the mainnet pipeline +> later, add `USDtz` back to `REQUIRED_CANONICAL_KEYS` (and to the allowlist) in +> `mainnet_preflight.js`. + +### Deployment Manifest + +The deploy scripts track originated addresses in a manifest file +([`TezFinBuild/deploy_result/deploy.json`](TezFinBuild/deploy_result/deploy.json) by default). This +file also stores the `chainId` it was created against; the deployer refuses to reuse a manifest +whose `chainId` doesn't match the connected RPC. + +- The tracked `deploy.json` is the populated **Previewnet deployment record** and may be committed so + an interrupted Previewnet deployment can resume. Do not edit or replace its addresses manually: + existing entries are reused only after each address is verified on-chain (matching code and + critical storage addresses, plus all immutable IRM rate parameters), never silently. Mainnet + deployment records must use `deploy.mainnet.json` (or another explicit `DEPLOY_MANIFEST` path) and + must never be written to `deploy.json`. +- The manifest path resolution is centralized (`resolveDeployResultPath()` in `util.js`, mirrored by + `Config.py` for the SmartPy side) so every tool agrees on the same file: + 1. `DEPLOY_MANIFEST`, if set, always wins. + 2. Otherwise, the default is derived from `deploy_script/config.json`'s `networkProfile`: + `deploy.mainnet.json` when it's `"mainnet"`, `deploy.json` otherwise. + To keep Previewnet and mainnet deployments in fully separate files, set `DEPLOY_MANIFEST` to an + explicit path before running `npm run prepare:deploy` and the deploy shell scripts, e.g.: + ```sh + export DEPLOY_MANIFEST=TezFinBuild/deploy_result/deploy.mainnet.json + ``` + Never reuse a Previewnet manifest for a mainnet run (or vice versa). +- If you need to restart a deployment from scratch on the same network, delete or rename the + manifest file first rather than editing it in place. + +### PriceOracle Configuration + +`TezFinOracle` (and therefore `Comptroller`) requires a `PriceOracle` address in the manifest before +it can be compiled — `CompileTezFinOracle.py` validates this dependency and fails with a clear error +if `PriceOracle` is missing, instead of silently compiling with a stale value. Both deploy scripts +also run `verify_oracle.js` right before compiling `TezFinOracle`, which checks that the configured +`PriceOracle` address actually exists on the connected chain (not just that the manifest key is +present) and fails closed if it does not. + +`TezFinOracle` ([`contracts/TezFinOracle.py`](contracts/TezFinOracle.py)) is a thin proxy: it forwards +price lookups to the address stored as `oracle` (the `PriceOracle` from the manifest) and expects that +address to expose the on-chain view `get_price_with_timestamp(string) -> pair(nat, timestamp)` for +symbols such as `XTZUSDT` and `BTCUSDT`. It also has a small admin-controlled override map for assets +the upstream feed does not support (e.g. USD and USDT). `TezFinOracle`'s own `admin` (settable via +`set_pending_admin` / `accept_admin`) controls those overrides and can repoint `oracle` to a different +feed with `set_oracle`. + +- **Previewnet**: `CompileTestData.py` compiles and deploys a mock `PriceOracle` + ([`deploy/test_data/PriceOracle.py`](deploy/test_data/PriceOracle.py)) as part of + `deploy_previewnet.sh`. This mock is for Previewnet only, is **not** Harbinger — it's a bare + stand-in that mimics the same `get` callback interface. It has **no administrator check**: its + `setPrice` entry point can be called by any address to set any price for any asset. Do not treat a + Previewnet deployment using this mock as representative of mainnet price-feed security. +- **Mainnet**: `deploy_mainnet.sh` never compiles or originates the mock oracle (it does not run + `CompileTestData.py` at all). Put the exact address of the vetted production Harbinger (or + Harbinger-compatible) oracle directly under the `PriceOracle` key in the mainnet manifest + (`DEPLOY_MANIFEST`) before running `deploy_mainnet.sh`; `mainnet_preflight.js` verifies it exists + on-chain before anything is compiled. The mandatory programmatic deployment preflight executes the + exact XTZ, USDT, and tzBTC views before origination and rejects zero, stale, or + future/millisecond timestamps. Document, + alongside the mainnet manifest, which oracle instance/administrator is being used and who controls + it — this project does not deploy or administer that upstream feed itself. + +## Post-Deployment Admin Handoff (Mainnet) + +After origination, every contract (`Governance`, `TezFinOracle`) is initially administered by the +deployer account (`OriginatorAddress`). Before any market is unpaused or opened to real users, the +protocol admin rights **must** be handed off to the production multisig. This is currently a manual +process: + +1. Keep all markets paused until the handoff below is fully verified. +2. On `Governance`, call `setPendingGovernance()` from the deployer account. +3. From the multisig, call `acceptGovernance()` on `Governance` to finalize the transfer. +4. On `TezFinOracle`, call `set_pending_admin()` from the deployer account, then + have the multisig call `accept_admin()` to finalize the transfer. +5. Verify the exact final on-chain storage (e.g. via a block explorer or a Taquito script): + - `Governance.administrator` is the production multisig. + - `TezFinOracle.admin` is the production multisig. + - `Comptroller.administrator` is the `Governance` contract address. + - Every ꜰToken's `administrator` is the `Governance` contract address. + - `pendingAdministrator` is `None` on `Governance`, `Comptroller`, and every ꜰToken, and + `TezFinOracle.pendingAdmin` is `None`. + - The deployment wallet is absent from all administrator and pending-administrator fields and + retains no administrative authority. +6. Only unpause markets after every check in step 5 passes. + +Note: `Comptroller` and every ꜰToken market are administered *through* `Governance` +(`Governance.setContractGovernance` / `acceptContractGovernance` act as a proxy for their +`setPendingGovernance` / `acceptGovernance` entry points). Once `Governance` itself is controlled by +the production multisig (steps 2-3), the multisig automatically controls Comptroller and every +ꜰToken through it — there is no separate handoff needed for those contracts, only the on-chain +verification in step 5. + ## Build and Run UI @@ -122,4 +384,3 @@ npm install cd src/ui npm start ``` - diff --git a/TezFinBuild/deploy_result/deploy.json b/TezFinBuild/deploy_result/deploy.json index 9537d2ea..e9c9357f 100644 --- a/TezFinBuild/deploy_result/deploy.json +++ b/TezFinBuild/deploy_result/deploy.json @@ -1,18 +1,19 @@ { - "OriginatorAddress": "tz1RESHvJAfmQCXCAD3ubNmVtac788pnN1oL", - "Governance": "KT1NF6DKX5giazRTzPtEuNX1npkVcaoQkvK2", - "PriceOracle": "KT1B74ywpG3nX2F3ZgpYi4MnsyuoYyQtQTAn", - "USDTPriceOracle": "KT1B74ywpG3nX2F3ZgpYi4MnsyuoYyQtQTAn", - "TezFinOracle": "KT1LDtNKDnpziwSrmSfx3xCbs7nBMg7VFp4m", - "Comptroller": "KT1DiWBT6RBC97iWrvLHRzKL7AWQKorBiuRG", - "CFA12_IRM": "KT1Vzf3fXVBry4TDxWAfYzsn6ZPyMroMKUdW", - "CXTZ_IRM": "KT1B7zvU7EXmPBHazHhtajHaw5swFFxWCEfd", - "CFA2_IRM": "KT1Q2BBtfT9obGMAZ32L6esSjm8FG9NWiBb9", - "USDtz": "KT1LN4LPSqTMS7Sd2CJw4bbDGRkMv2t68Fy9", - "USDt": "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o", - "tzBTC": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", - "CUSDt": "KT1HCRJhfqmWKRJtZXzvTkY4iisfuR4w6pkB", - "CUSDtz": "KT1WQM7wj64GHCndwV8REccQ6N4tqZ3uRNqs", - "CXTZ": "KT1MCXxbtS62tk4CUxv29BHnqTBtvsFFGzBm", - "CtzBTC": "KT19gZac3vqV3ZeMJbhMX7Xy8kcocKK4Tbz1" + "OriginatorAddress": "tz1XTWbfhyWK9xmPAa6TyQUSv437JFgZDzgA", + "chainId": "NetXY2oPPzkxUW1", + "network": "https://michelson.previewnet.tezosx.nomadic-labs.com", + "PriceOracle": "KT1SyeZTavgHjmk1zBBJZ1aVx5F5NH27shJG", + "USDt": "KT1WWKLMc2aAanNxvYR3GcLpArTnYG6sbzzp", + "USDtz": "KT1NyMkX6ddVdZJmQgM826EApDLQanvuJ4LG", + "tzBTC": "KT1N8UrMt8bwJTphb5NSpFhB7GDFiYbzJFLz", + "TezFinOracle": "KT1V4ArruDLGTjGYC5Xgd9habQ2SpDtrmkDF", + "Governance": "KT1Wq7uJeiXXociunW4LqQZzdNvM7QYbtVEN", + "Comptroller": "KT1N8XU2pe51z22aMbRQya9YhPmCytyX2Met", + "CFA12_IRM": "KT1BntKHTG4So6RCT4CHHSXP1xNGLVj6krXE", + "CFA2_IRM": "KT1QT1P8rHnoirQquBV1cmTaWivgGXFxhYax", + "CXTZ_IRM": "KT1LEGHhjvLyun3E5zwBNsJRmTtCzCjVpUk6", + "CtzBTC_IRM": "KT1XkH9xQQnjGheHJ5eHzzVBTj4A119iR8yC", + "CUSDt": "KT1Cugpw5mGTQydYt2dCw2CUxsPsnGSdt7R9", + "CXTZ": "KT1DKYqH3v6AnQ9YTnCyJ44CnCvSG2pUogHH", + "CtzBTC": "KT1A45Vx1fwxwjBBfxzXBWmTkqEvnB2udq7J" } diff --git a/TezFinBuild/deploy_result/deploy.mainnet.json b/TezFinBuild/deploy_result/deploy.mainnet.json new file mode 100644 index 00000000..47e293f7 --- /dev/null +++ b/TezFinBuild/deploy_result/deploy.mainnet.json @@ -0,0 +1,8 @@ +{ + "OriginatorAddress": "tz1XTWbfhyWK9xmPAa6TyQUSv437JFgZDzgA", + "chainId": "NetXdQprcVkpaWU", + "network": "https://rpc.tzkt.io/mainnet", + "PriceOracle": null, + "USDt": "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o", + "tzBTC": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn" +} diff --git a/contracts/CToken.py b/contracts/CToken.py index a2b26df5..a5eeedb2 100644 --- a/contracts/CToken.py +++ b/contracts/CToken.py @@ -546,6 +546,13 @@ def getTotalSupply(self, params): sp.set_type(params, sp.TUnit) sp.result(self.data.totalSupply) + @sp.onchain_view() + def marketTotals(self, params): + sp.set_type(params, sp.TUnit) + suppliedUnderlying = self.mulScalarTruncate( + self.makeExp(self.exchangeRateStoredImpl()), self.data.totalSupply) + sp.result(sp.pair(suppliedUnderlying, self.data.totalBorrows)) + """ Get the current allowance from `owner` for `spender` diff --git a/contracts/Comptroller.py b/contracts/Comptroller.py index af334fa4..8da6529a 100644 --- a/contracts/Comptroller.py +++ b/contracts/Comptroller.py @@ -14,9 +14,6 @@ "file:contracts/utils/Exponential.py") OP = sp.io.import_script_from_url("file:contracts/utils/OperationProtector.py") -SweepTokens = sp.io.import_script_from_url( - "file:contracts/utils/SweepTokens.py") - TMarket = sp.TRecord(isListed=sp.TBool, # Whether or not this market is listed # Multiplier representing the most one can borrow against their collateral in this market. collateralFactor=Exponential.TExp, @@ -25,6 +22,9 @@ mintPaused=sp.TBool, borrowPaused=sp.TBool, redeemPaused=sp.TBool, + liquidatePaused=sp.TBool, + supplyCap=sp.TNat, + borrowCap=sp.TNat, name=sp.TString, # Asset name for price oracle price=Exponential.TExp, # The price of the asset priceExp=sp.TNat, # exponent needed to normalize the token prices to 10^18 @@ -41,7 +41,7 @@ DEFAULT_COLLATERAL_FACTOR = int(5e17) # 50 % -class Comptroller(CMPTInterface.ComptrollerInterface, Exponential.Exponential, SweepTokens.SweepTokens, OP.OperationProtector): +class Comptroller(CMPTInterface.ComptrollerInterface, Exponential.Exponential, OP.OperationProtector): def __init__(self, administrator_, oracleAddress_, closeFactorMantissa_, liquidationIncentiveMantissa_, maxAssetsPerUser_=15, **extra_storage): Exponential.Exponential.__init__( self, @@ -66,7 +66,6 @@ def __init__(self, administrator_, oracleAddress_, closeFactorMantissa_, liquida activeOperations=sp.set(t=sp.TNat), closeFactorMantissa=closeFactorMantissa_, liquidationIncentiveMantissa=liquidationIncentiveMantissa_, - maxPriceTimeDifference = sp.int(86400), maxAssetsPerUser=maxAssetsPerUser_, **extra_storage ) @@ -76,10 +75,9 @@ def __init__(self, administrator_, oracleAddress_, closeFactorMantissa_, liquida cTokens: TList(TAddress) - The list of addresses of the cToken markets to be enabled """ - @sp.entry_point(lazify=True) + @sp.entry_point def enterMarkets(self, cTokens): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(cTokens, sp.TList(sp.TAddress)) currentAssetCount = sp.local("currentAssetCount", self.getUserUniqueAssetsCount(sp.sender)) sp.for token in cTokens: @@ -110,8 +108,7 @@ def addToCollaterals(self, cToken, lender): """ @sp.entry_point def exitMarket(self, cToken): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(cToken, sp.TAddress) self.activateOp(OP.ComptrollerOperations.EXIT_MARKET) @@ -125,10 +122,9 @@ def exitMarket(self, cToken): accountSnapshot: TAccountSnapshot - Container for account balance information """ - @sp.entry_point(lazify=True) + @sp.entry_point def setAccountSnapAndExitMarket(self, accountSnapshot): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(accountSnapshot, CTI.TAccountSnapshot) self.verifyAndFinishActiveOp(OP.ComptrollerOperations.EXIT_MARKET) sp.verify(accountSnapshot.borrowBalance == @@ -152,14 +148,19 @@ def setAccountSnapAndExitMarket(self, accountSnapshot): minter: TAddress - The account which would get the minted tokens mintAmount: TNat - The amount of underlying being supplied to the market in exchange for tokens """ - @sp.entry_point(lazify=True) + @sp.entry_point def mintAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TMintAllowedParams) sp.verify( ~ self.data.markets[params.cToken].mintPaused, EC.CMPT_MINT_PAUSED) self.verifyMarketListed(params.cToken) + marketTotals = sp.view("marketTotals", params.cToken, sp.unit, + t=sp.TPair(sp.TNat, sp.TNat)).open_some( + "INVALID MARKET TOTALS VIEW") + sp.verify(sp.fst(marketTotals) <= + self.data.markets[params.cToken].supplyCap, + EC.CMPT_SUPPLY_CAP_EXCEEDED) self.invalidateLiquidity(params.minter) """ @@ -175,10 +176,9 @@ def mintAllowed(self, params): redeemTokens: TNat - The number of cTokens removed from the redeemer exchangeRateMantissa: TNat - The pre-redemption exchange rate, scaled by 1e18 """ - @sp.entry_point(lazify=True) + @sp.entry_point def redeemAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TRedeemAllowedParams) self.verifyMarketListed(params.cToken) sp.verify( @@ -248,14 +248,19 @@ def checkCollateralReductionInternal(self, cToken, account, exchangeRateMantissa borrower: TAddress - The account which would borrow the tokens borrowAmount: TNat - The amount of underlying the account would borrow """ - @sp.entry_point(lazify=True) + @sp.entry_point def borrowAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TBorrowAllowedParams) sp.verify( ~ self.data.markets[params.cToken].borrowPaused, EC.CMPT_BORROW_PAUSED) self.verifyMarketListed(params.cToken) + marketTotals = sp.view("marketTotals", params.cToken, sp.unit, + t=sp.TPair(sp.TNat, sp.TNat)).open_some( + "INVALID MARKET TOTALS VIEW") + sp.verify(sp.snd(marketTotals) <= + self.data.markets[params.cToken].borrowCap, + EC.CMPT_BORROW_CAP_EXCEEDED) sp.if self.isNewAssetForUser(params.borrower, params.cToken): uniqueAssetsCount = self.getUserUniqueAssetsCount(params.borrower) sp.verify(uniqueAssetsCount < self.data.maxAssetsPerUser, @@ -266,6 +271,15 @@ def borrowAllowed(self, params): EC.CMPT_INVALID_BORROW_SENDER) sp.if sp.sender == params.cToken: self.addToLoans(params.cToken, params.borrower) + sp.transfer(sp.set([params.cToken]), sp.mutez(0), sp.self_entry_point( + "updateAssetPricesWithView")) + sp.transfer(params, sp.mutez(0), sp.self_entry_point( + "completeBorrowAllowed")) + + @sp.entry_point + def completeBorrowAllowed(self, params): + sp.set_type(params, CMPTInterface.TBorrowAllowedParams) + sp.verify(sp.sender == sp.self_address, EC.CMPT_INVALID_BORROW_SENDER) self.checkInsuffLiquidityInternal( params.cToken, params.borrower, params.borrowAmount) self.invalidateLiquidity(params.borrower) @@ -281,10 +295,9 @@ def addToLoans(self, cToken, borrower): sp.else: self.data.loans[borrower] = sp.set([cToken]) - @sp.entry_point(lazify=True) + @sp.entry_point def removeFromLoans(self, borrower): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") self.verifyMarketListed(sp.sender) sp.if self.data.loans.contains(borrower) & self.data.loans[borrower].contains(sp.sender): self.data.loans[borrower].remove(sp.sender) @@ -298,10 +311,9 @@ def removeFromLoans(self, borrower): borrower: TAddress - The account which would borrowed the asset repayAmount: TNat - The amount of the underlying asset the account would repay """ - @sp.entry_point(lazify=True) + @sp.entry_point def repayBorrowAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TRepayBorrowAllowedParams) self.verifyMarketListed(params.cToken) self.invalidateLiquidity(params.borrower) @@ -320,10 +332,9 @@ def repayBorrowAllowed(self, params): dst: TAddress - The account which receives the tokens transferTokens: TNat - The number of cTokens to transfer """ - @sp.entry_point(lazify=True) + @sp.entry_point def transferAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TTransferAllowedParams) sp.verify(~ self.data.transferPaused, EC.CMPT_TRANSFER_PAUSED) self.verifyMarketListed(params.cToken) @@ -358,22 +369,39 @@ def transferAllowed(self, params): """ @sp.entry_point def updateAllAssetPricesWithView(self): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") self.updateAllAssetPrices() def updateAllAssetPrices(self): + assets = sp.local("assets", sp.set(t=sp.TAddress)) sp.for asset in self.data.marketNameToAddress.values(): + sp.if self.data.markets[asset].isListed: + assets.value.add(asset) + sp.transfer(assets.value, sp.mutez(0), sp.self_entry_point( + "updateAssetPricesWithView")) + + @sp.entry_point + def updateAssetPricesWithView(self, assets): + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") + sp.set_type(assets, sp.TSet(sp.TAddress)) + sp.for asset in assets.elements(): + self.verifyMarketListed(asset) sp.if self.data.markets[asset].updateLevel < sp.level: - pricePair = sp.local("pricePair", - sp.view("getPrice", self.data.oracleAddress, self.data.markets[asset].name + "-USD", + previousRawPrice = self.data.markets[asset].price.mantissa // self.data.markets[asset].priceExp + pricePair = sp.local("pricePair", + sp.view("getValidatedPrice", self.data.oracleAddress, + sp.record(comptroller=sp.self_address, + cToken=asset, + requestedAsset=self.data.markets[asset].name + "-USD", + previousPrice=previousRawPrice, + previousTimestamp=self.data.markets[asset].priceTimestamp), t=sp.TPair(sp.TTimestamp, sp.TNat)).open_some("invalid oracle view call") ) - sp.if self.data.markets[asset].priceTimestamp!= sp.timestamp(0): - sp.verify(sp.now - sp.fst(pricePair.value) <= self.data.maxPriceTimeDifference, "STALE_ASSET_PRICE") + priceTimestamp = sp.fst(pricePair.value) + rawPrice = sp.snd(pricePair.value) self.data.markets[asset].price = self.makeExp( - sp.snd(pricePair.value)*self.data.markets[asset].priceExp) - self.data.markets[asset].priceTimestamp = sp.fst(pricePair.value) + rawPrice*self.data.markets[asset].priceExp) + self.data.markets[asset].priceTimestamp = priceTimestamp self.data.markets[asset].updateLevel = sp.level def getAssetPrice(self, asset): @@ -387,11 +415,20 @@ def getAssetPrice(self, asset): """ @sp.entry_point def updateAccountLiquidityWithView(self, account): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(account, sp.TAddress) - self.updateAllAssetPrices() - self.accrueAllAssetInterests() + accountAssets = sp.local("accountAssets", sp.set(t=sp.TAddress)) + sp.if self.data.collaterals.contains(account): + sp.for asset in self.data.collaterals[account].elements(): + accountAssets.value.add(asset) + sp.if self.data.loans.contains(account): + sp.for asset in self.data.loans[account].elements(): + accountAssets.value.add(asset) + sp.transfer(accountAssets.value, sp.mutez(0), sp.self_entry_point( + "updateAssetPricesWithView")) + sp.for asset in accountAssets.value.elements(): + sp.transfer(sp.unit, sp.mutez(0), sp.contract( + sp.TUnit, asset, entry_point="accrueInterest").open_some()) self.activateOp(OP.ComptrollerOperations.SET_LIQUIDITY) sp.transfer(account, sp.mutez(0), sp.self_entry_point( "setAccountLiquidityWithView")) @@ -401,10 +438,9 @@ def updateAccountLiquidityWithView(self, account): account: TAddress - The account to calculate liquidity for """ - @sp.entry_point(lazify=True) + @sp.entry_point def setAccountLiquidityWithView(self, account): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(account, sp.TAddress) liquidity = sp.local( 'liquidity', self.calculateCurrentAccountLiquidityWithView(account)).value @@ -415,11 +451,6 @@ def setAccountLiquidityWithView(self, account): valid=True ) - def accrueAllAssetInterests(self): - sp.for asset in self.data.marketNameToAddress.values(): - sp.transfer(sp.unit, sp.mutez(0), sp.contract( - sp.TUnit, asset, entry_point="accrueInterest").open_some()) - """ Determine what the account liquidity would be if the given amounts were redeemed/borrowed, updates asset prices and accrues interests if stale @@ -430,7 +461,6 @@ def accrueAllAssetInterests(self): # def getHypoAccountLiquidity(self, params): # sp.set_type(params, CMPTInterface.TGetAccountLiquidityParams) # self.updateAllAssetPrices() - # self.accrueAllAssetInterests() # sp.transfer((params.data, params.callback), sp.mutez( # 0), sp.self_entry_point("returnHypoAccountLiquidity")) @@ -499,14 +529,17 @@ def liquidateCalculateSeizeTokens(self, params): updateAccountLiquidityWithView() needs to be called before executing this to get up-to-date results """ - @sp.entry_point(lazify=True) + @sp.entry_point def liquidateBorrowAllowed(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, CMPTInterface.TLiquidateBorrowAllowed) self.verifyMarketListed(params.cTokenBorrowed) self.verifyMarketListed(params.cTokenCollateral) + sp.verify(~self.data.markets[params.cTokenBorrowed].liquidatePaused, + EC.CMPT_LIQUIDATE_PAUSED) + sp.verify(~self.data.markets[params.cTokenCollateral].liquidatePaused, + EC.CMPT_LIQUIDATE_PAUSED) liquidity = sp.local( "liquidtiy", self.getAccountLiquidityInternal(params.borrower)) @@ -571,16 +604,16 @@ def calculateAccountLiquidityWithView(self, params): calculation.sumBorrowPlusEffects += temp.sumBorrowPlusEffects sp.if self.data.loans.contains(params.account): sp.for asset in self.data.loans[params.account].elements(): - # only get liquidity for assets that aren't in collaterals to avoid double counting + # only get liquidity for assets that aren't in collaterals to avoid double counting. + # Deciding that up front, rather than calling the helper from both branches of the + # collaterals check, keeps a single inlined copy of the (large) per-asset view call. + alreadyCounted = sp.local("alreadyCounted", False) sp.if self.data.collaterals.contains(params.account): - sp.if ~self.data.collaterals[params.account].contains(asset): - # cToken.accrueInterest() for the given asset should be executed within the same block prior to this call - # updateAssetPrice() should be executed within the same block prior to this call - temp = self.calculateAccountAssetLiquidityView( - asset, params.account) - calculation.sumCollateral += temp.sumCollateral - calculation.sumBorrowPlusEffects += temp.sumBorrowPlusEffects - sp.else: + sp.if self.data.collaterals[params.account].contains(asset): + alreadyCounted.value = True + sp.if ~alreadyCounted.value: + # cToken.accrueInterest() for the given asset should be executed within the same block prior to this call + # updateAssetPrice() should be executed within the same block prior to this call temp = self.calculateAccountAssetLiquidityView( asset, params.account) calculation.sumCollateral += temp.sumCollateral @@ -631,10 +664,9 @@ def getMaxAssetsPerUser(self): params: TAddress - The address of the new pending governance contract """ - @sp.entry_point(lazify=True) + @sp.entry_point def setPendingGovernance(self, pendingAdminAddress): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(pendingAdminAddress, sp.TAddress) self.verifyAdministrator() self.data.pendingAdministrator = sp.some(pendingAdminAddress) @@ -646,10 +678,9 @@ def setPendingGovernance(self, pendingAdminAddress): params: TUnit """ - @sp.entry_point(lazify=True) + @sp.entry_point def acceptGovernance(self, unusedArg): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(unusedArg, sp.TUnit) sp.verify(sp.sender == self.data.pendingAdministrator.open_some( EC.CMPT_NOT_SET_PENDING_ADMIN), EC.CMPT_NOT_PENDING_ADMIN) @@ -665,10 +696,9 @@ def acceptGovernance(self, unusedArg): cToken: TAddress - The address of the market to change the mint pause state state: TBool - state, where True - pause, False - activate """ - @sp.entry_point(lazify=True) + @sp.entry_point def setMintPaused(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord(cToken=sp.TAddress, state=sp.TBool)) self.verifyMarketListed(params.cToken) self.verifyAdministrator() @@ -683,10 +713,9 @@ def setMintPaused(self, params): cToken: TAddress - The address of the market to change the borrow pause state state: TBool - state, where True - pause, False - activate """ - @sp.entry_point(lazify=True) + @sp.entry_point def setBorrowPaused(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord(cToken=sp.TAddress, state=sp.TBool)) self.verifyMarketListed(params.cToken) self.verifyAdministrator() @@ -695,15 +724,32 @@ def setBorrowPaused(self, params): """ Pause or activate redemptions of a given CToken. """ - @sp.entry_point(lazify=True) + @sp.entry_point def setRedeemPaused(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord(cToken=sp.TAddress, state=sp.TBool)) self.verifyMarketListed(params.cToken) self.verifyAdministrator() self.data.markets[params.cToken].redeemPaused = params.state + @sp.entry_point + def setLiquidatePaused(self, params): + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") + sp.set_type(params, sp.TRecord(cToken=sp.TAddress, state=sp.TBool)) + self.verifyMarketListed(params.cToken) + self.verifyAdministrator() + self.data.markets[params.cToken].liquidatePaused = params.state + + @sp.entry_point + def setMarketCaps(self, params): + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") + sp.set_type(params, sp.TRecord(cToken=sp.TAddress, supplyCap=sp.TNat, + borrowCap=sp.TNat)) + self.verifyMarketListed(params.cToken) + self.verifyAdministrator() + self.data.markets[params.cToken].supplyCap = params.supplyCap + self.data.markets[params.cToken].borrowCap = params.borrowCap + """ Pause or activate the transfer of CTokens @@ -711,10 +757,9 @@ def setRedeemPaused(self, params): state: TBool, where True - pause, False - activate """ - @sp.entry_point(lazify=True) + @sp.entry_point def setTransferPaused(self, state): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(state, sp.TBool) self.verifyAdministrator() self.data.transferPaused = state @@ -728,14 +773,26 @@ def setTransferPaused(self, state): NOTE: can only use harbinger or harbinger like oracle """ - @sp.entry_point(lazify=True) + @sp.entry_point def setPriceOracleAndTimeDiff(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord(priceOracle=sp.TAddress, timeDiff=sp.TInt)) self.verifyAdministrator() - self.data.maxPriceTimeDifference = params.timeDiff self.data.oracleAddress = params.priceOracle + destination = sp.contract(sp.TInt, params.priceOracle, + "configureMaxPriceAge").open_some() + sp.transfer(params.timeDiff, sp.mutez(0), destination) + + @sp.entry_point + def setPriceBounds(self, params): + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") + sp.set_type(params, sp.TRecord(cToken=sp.TAddress, minPrice=sp.TNat, + maxPrice=sp.TNat, maxChangeBps=sp.TNat)) + self.verifyAdministrator() + destination = sp.contract(OracleInterface.TPriceBounds, + self.data.oracleAddress, + "configurePriceBounds").open_some() + sp.transfer(params, sp.mutez(0), destination) """ Sets the closeFactor used when liquidating borrows @@ -744,10 +801,9 @@ def setPriceOracleAndTimeDiff(self, params): closeFactorMantissa: TNat - New close factor, scaled by 1e18 """ - @sp.entry_point(lazify=True) + @sp.entry_point def setCloseFactor(self, closeFactorMantissa): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(closeFactorMantissa, sp.TNat) self.verifyAdministrator() self.data.closeFactorMantissa = closeFactorMantissa @@ -759,10 +815,9 @@ def setCloseFactor(self, closeFactorMantissa): newLimit: TNat - The new limit for the maximum number of unique assets a user can hold """ - @sp.entry_point(lazify=True) + @sp.entry_point def setMaxAssetsPerUser(self, newLimit): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(newLimit, sp.TNat) self.verifyAdministrator() self.data.maxAssetsPerUser = newLimit @@ -776,10 +831,9 @@ def setMaxAssetsPerUser(self, newLimit): cToken: TAddress - The market to set the factor on newCollateralFactor: TNat - The new collateral factor, scaled by 1e18 """ - @sp.entry_point(lazify=True) + @sp.entry_point def setCollateralFactor(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord( cToken=sp.TAddress, newCollateralFactor=sp.TNat)) self.verifyAdministrator() @@ -795,10 +849,9 @@ def setCollateralFactor(self, params): liquidationIncentiveMantissa: TNat - New liquidationIncentive scaled by 1e18 """ - @sp.entry_point(lazify=True) + @sp.entry_point def setLiquidationIncentive(self, liquidationIncentiveMantissa): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(liquidationIncentiveMantissa, sp.TNat) self.verifyAdministrator() self.data.liquidationIncentiveMantissa = liquidationIncentiveMantissa @@ -813,14 +866,14 @@ def setLiquidationIncentive(self, liquidationIncentiveMantissa): name: TString - The market name in price oracle priceExp: TNat - exponent needed to normalize the token prices to 10^18 (eth:0, btc: 10, usd: 12) """ - @sp.entry_point(lazify=True) + @sp.entry_point def supportMarket(self, params): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(params, sp.TRecord(cToken=sp.TAddress, name=sp.TString, priceExp=sp.TNat)) self.verifyAdministrator() self.verifyMarketNotListed(params.cToken) + sp.verify(params.priceExp > 0, "INVALID_PRICE_EXP") self.data.markets[params.cToken] = sp.record(isListed=sp.bool(True), collateralFactor=self.makeExp( sp.nat(DEFAULT_COLLATERAL_FACTOR)), @@ -828,6 +881,9 @@ def supportMarket(self, params): borrowPaused=sp.bool( True), redeemPaused=sp.bool(True), + liquidatePaused=sp.bool(True), + supplyCap=sp.nat(0), + borrowCap=sp.nat(0), name=params.name, price=self.makeExp( sp.nat(0)), @@ -843,10 +899,9 @@ def supportMarket(self, params): cToken: TAddress - The address of the market (token) to disable """ - @sp.entry_point(lazify=True) + @sp.entry_point def disableMarket(self, cToken): - sp.verify(sp.amount == sp.utils.nat_to_mutez( - 0), "TEZ_TRANSFERED") + sp.verify(sp.amount == sp.mutez(0), "TEZ_TRANSFERED") sp.set_type(cToken, sp.TAddress) self.verifyAdministrator() self.verifyMarketListed(cToken) diff --git a/contracts/Governance.py b/contracts/Governance.py index e2a3d5bb..19fd2eee 100644 --- a/contracts/Governance.py +++ b/contracts/Governance.py @@ -220,6 +220,18 @@ def setPriceOracleAndTimeDiff(self, params): sp.TRecord(priceOracle=sp.TAddress, timeDiff=sp.TInt), params.comptroller, "setPriceOracleAndTimeDiff").open_some() sp.transfer(sp.record(priceOracle=params.priceOracle,timeDiff=params.timeDiff), sp.mutez(0), contract) + @sp.entry_point + def setPriceBounds(self, params): + self.verifyAdministrator() + sp.set_type(params, sp.TRecord(comptroller=sp.TAddress, + bounds=sp.TRecord(cToken=sp.TAddress, minPrice=sp.TNat, + maxPrice=sp.TNat, maxChangeBps=sp.TNat))) + contract = sp.contract(sp.TRecord(cToken=sp.TAddress, + minPrice=sp.TNat, maxPrice=sp.TNat, + maxChangeBps=sp.TNat), params.comptroller, + "setPriceBounds").open_some() + sp.transfer(params.bounds, sp.mutez(0), contract) + """ Sets the closeFactor used when liquidating borrows @@ -371,6 +383,26 @@ def setRedeemPaused(self, params): "setRedeemPaused").open_some() sp.transfer(params.tokenState, sp.mutez(0), contract) + @sp.entry_point + def setLiquidatePaused(self, params): + self.verifyAdministrator() + sp.set_type(params, sp.TRecord(comptroller=sp.TAddress, + tokenState=sp.TRecord(cToken=sp.TAddress, state=sp.TBool))) + contract = sp.contract(sp.TRecord(cToken=sp.TAddress, state=sp.TBool), + params.comptroller, "setLiquidatePaused").open_some() + sp.transfer(params.tokenState, sp.mutez(0), contract) + + @sp.entry_point + def setMarketCaps(self, params): + self.verifyAdministrator() + sp.set_type(params, sp.TRecord(comptroller=sp.TAddress, + caps=sp.TRecord(cToken=sp.TAddress, supplyCap=sp.TNat, + borrowCap=sp.TNat))) + contract = sp.contract(sp.TRecord(cToken=sp.TAddress, + supplyCap=sp.TNat, borrowCap=sp.TNat), + params.comptroller, "setMarketCaps").open_some() + sp.transfer(params.caps, sp.mutez(0), contract) + """ Pause or activate the transfer of CTokens diff --git a/contracts/TezFinOracle.py b/contracts/TezFinOracle.py index f31b64d8..b11a88cb 100644 --- a/contracts/TezFinOracle.py +++ b/contracts/TezFinOracle.py @@ -1,5 +1,4 @@ import smartpy as sp -import time OracleInterface = sp.io.import_script_from_url( "file:contracts/interfaces/OracleInterface.py") @@ -12,10 +11,13 @@ class TezFinOracle(OracleInterface.OracleInterface): def __init__(self, admin, oracle): self.init( - overrides=sp.big_map(l={"USD-USD": (sp.timestamp(int(time.time())), sp.as_nat( - 1000000)),"USDT-USD": (sp.timestamp(int(time.time())), sp.as_nat( - 1000000))}, tkey=sp.TString, tvalue=sp.TPair(sp.TTimestamp, sp.TNat)), - alias=sp.big_map(l={"OXTZ-USD": "XTZ-USD", "WTZ-USD": "XTZ-USD", "TZBTC-USD":"BTC-USD", "STXTZ-USD": "XTZ-USD"}, + overrides=sp.big_map(l={}, tkey=sp.TString, + tvalue=sp.TPair(sp.TTimestamp, sp.TNat)), + priceBounds=sp.big_map( + l={}, tkey=sp.TPair(sp.TAddress, sp.TAddress), + tvalue=OracleInterface.TPriceBounds), + maxPriceAge=sp.big_map(l={}, tkey=sp.TAddress, tvalue=sp.TInt), + alias=sp.big_map(l={"OXTZ-USD": "XTZ-USD", "WTZ-USD": "XTZ-USD", "STXTZ-USD": "XTZ-USD"}, tkey=sp.TString, tvalue=sp.TString), oracle=oracle, admin=admin, @@ -85,6 +87,22 @@ def removeAlias(self, asset): sp.verify(self.is_admin(sp.sender), message="NOT_ADMIN") del self.data.alias[asset] + @sp.entry_point + def configurePriceBounds(self, params): + sp.set_type(params, OracleInterface.TPriceBounds) + sp.verify((params.minPrice > 0) & + (params.minPrice <= params.maxPrice) & + (params.maxChangeBps <= 10000), + "INVALID_PRICE_BOUNDS") + self.data.priceBounds[sp.pair(sp.sender, params.cToken)] = params + + @sp.entry_point + def configureMaxPriceAge(self, maxPriceAge): + sp.set_type(maxPriceAge, sp.TInt) + sp.verify((maxPriceAge > 0) & (maxPriceAge <= 3600), + "INVALID_MAX_PRICE_TIME_DIFFERENCE") + self.data.maxPriceAge[sp.sender] = maxPriceAge + @sp.onchain_view() def get_price_with_timestamp(self, requestedAsset): """ @@ -92,7 +110,8 @@ def get_price_with_timestamp(self, requestedAsset): """ sp.set_type(requestedAsset, sp.TString) sp.if self.data.overrides.contains(requestedAsset): - sp.result((sp.snd(self.data.overrides[requestedAsset]), sp.now)) + sp.result((sp.snd(self.data.overrides[requestedAsset]), + sp.fst(self.data.overrides[requestedAsset]))) sp.else: asset = sp.local("asset", requestedAsset) sp.if self.data.alias.contains(requestedAsset): @@ -109,7 +128,7 @@ def getPrice(self, requestedAsset): """ sp.set_type(requestedAsset, sp.TString) sp.if self.data.overrides.contains(requestedAsset): - sp.result((sp.now, sp.snd(self.data.overrides[requestedAsset]))) + sp.result(self.data.overrides[requestedAsset]) sp.else: asset = sp.local("asset", requestedAsset) sp.if self.data.alias.contains(requestedAsset): @@ -117,4 +136,41 @@ def getPrice(self, requestedAsset): sliced_asset = sp.slice(asset.value, 0, sp.as_nat(sp.len(asset.value) - 4)).open_some("failed to convert asset name") oracle_data = sp.view("get_price_with_timestamp", self.data.oracle, sliced_asset+"USDT", t=sp.TPair( sp.TNat, sp.TTimestamp)).open_some("invalid oracle view call") - sp.result((sp.snd(oracle_data), sp.fst(oracle_data))) \ No newline at end of file + sp.result((sp.snd(oracle_data), sp.fst(oracle_data))) + + @sp.onchain_view() + def getValidatedPrice(self, params): + sp.set_type(params, OracleInterface.TValidatedPriceRequest) + configKey = sp.pair(params.comptroller, params.cToken) + sp.verify(self.data.priceBounds.contains(configKey), + "PRICE_BOUNDS_NOT_CONFIGURED") + sp.verify(self.data.maxPriceAge.contains(params.comptroller), + "MAX_PRICE_AGE_NOT_CONFIGURED") + pricePair = sp.view("getPrice", sp.self_address, params.requestedAsset, + t=sp.TPair(sp.TTimestamp, sp.TNat)).open_some( + "invalid oracle price view") + priceTimestamp = sp.fst(pricePair) + rawPrice = sp.snd(pricePair) + bounds = self.data.priceBounds[configKey] + sp.verify(priceTimestamp > sp.timestamp(0), + "INVALID_ASSET_PRICE_TIMESTAMP") + sp.verify(priceTimestamp <= sp.now, "FUTURE_ASSET_PRICE") + sp.verify(sp.now - priceTimestamp <= + self.data.maxPriceAge[params.comptroller], + "STALE_ASSET_PRICE") + sp.verify((params.previousTimestamp == sp.timestamp(0)) | + (priceTimestamp >= params.previousTimestamp), + "ASSET_PRICE_TIMESTAMP_ROLLBACK") + sp.verify((rawPrice >= bounds.minPrice) & + (rawPrice <= bounds.maxPrice), + "ASSET_PRICE_OUT_OF_BOUNDS") + sp.if params.previousTimestamp != sp.timestamp(0): + priceChange = sp.local("priceChange", sp.nat(0)) + sp.if rawPrice >= params.previousPrice: + priceChange.value = sp.as_nat(rawPrice - params.previousPrice) + sp.else: + priceChange.value = sp.as_nat(params.previousPrice - rawPrice) + sp.verify(priceChange.value * 10000 <= + params.previousPrice * bounds.maxChangeBps, + "ASSET_PRICE_CHANGE_TOO_LARGE") + sp.result(pricePair) \ No newline at end of file diff --git a/contracts/errors/ComptrollerErrors.py b/contracts/errors/ComptrollerErrors.py index 555159e1..1843c2b3 100644 --- a/contracts/errors/ComptrollerErrors.py +++ b/contracts/errors/ComptrollerErrors.py @@ -7,6 +7,9 @@ class ErrorCodes: CMPT_MINT_PAUSED = "CMPT_MINT_PAUSED" # Borrow is paused CMPT_BORROW_PAUSED = "CMPT_BORROW_PAUSED" + CMPT_LIQUIDATE_PAUSED = "CMPT_LIQUIDATE_PAUSED" + CMPT_SUPPLY_CAP_EXCEEDED = "CMPT_SUPPLY_CAP_EXCEEDED" + CMPT_BORROW_CAP_EXCEEDED = "CMPT_BORROW_CAP_EXCEEDED" # Redemption is paused CMPT_REDEEM_PAUSED = "CMPT_REDEEM_PAUSED" # Transfer is paused diff --git a/contracts/interfaces/ComptrollerInterface.py b/contracts/interfaces/ComptrollerInterface.py index a2b8a1ef..37471cef 100644 --- a/contracts/interfaces/ComptrollerInterface.py +++ b/contracts/interfaces/ComptrollerInterface.py @@ -187,6 +187,10 @@ def acceptGovernance(self, unusedArg): def setPriceOracleAndTimeDiff(self, params): pass + @sp.entry_point + def setPriceBounds(self, params): + pass + """ Sets the closeFactor used when liquidating borrows @@ -292,6 +296,14 @@ def setBorrowPaused(self, params): def setRedeemPaused(self, params): pass + @sp.entry_point + def setLiquidatePaused(self, params): + pass + + @sp.entry_point + def setMarketCaps(self, params): + pass + """ Pause or activate the transfer of CTokens diff --git a/contracts/interfaces/GovernanceInterface.py b/contracts/interfaces/GovernanceInterface.py index 84df12eb..98412726 100644 --- a/contracts/interfaces/GovernanceInterface.py +++ b/contracts/interfaces/GovernanceInterface.py @@ -113,6 +113,10 @@ def updateMetadata(self, params): def setPriceOracleAndTimeDiff(self, params): pass + @sp.entry_point + def setPriceBounds(self, params): + pass + """ Sets the closeFactor used when liquidating borrows @@ -206,6 +210,14 @@ def setBorrowPaused(self, params): def setRedeemPaused(self, params): pass + @sp.entry_point + def setLiquidatePaused(self, params): + pass + + @sp.entry_point + def setMarketCaps(self, params): + pass + """ Pause or activate the transfer of CTokens diff --git a/contracts/interfaces/OracleInterface.py b/contracts/interfaces/OracleInterface.py index aada233e..fe84d335 100644 --- a/contracts/interfaces/OracleInterface.py +++ b/contracts/interfaces/OracleInterface.py @@ -6,9 +6,21 @@ TSetPriceParam = sp.TPair(sp.TString, sp.TPair(sp.TTimestamp, sp.TNat)) TGetPriceParam = sp.TPair(sp.TString, sp.TContract(TSetPriceParam)) +TPriceBounds = sp.TRecord(cToken=sp.TAddress, minPrice=sp.TNat, + maxPrice=sp.TNat, maxChangeBps=sp.TNat) +TValidatedPriceRequest = sp.TRecord( + comptroller=sp.TAddress, + cToken=sp.TAddress, + requestedAsset=sp.TString, + previousPrice=sp.TNat, + previousTimestamp=sp.TTimestamp) class OracleInterface(sp.Contract): @sp.onchain_view() def getPrice(self, requestedAsset): pass + + @sp.onchain_view() + def getValidatedPrice(self, params): + pass diff --git a/contracts/tests/CapPostStateTest.py b/contracts/tests/CapPostStateTest.py new file mode 100644 index 00000000..68425485 --- /dev/null +++ b/contracts/tests/CapPostStateTest.py @@ -0,0 +1,158 @@ +import json + +import smartpy as sp + +CMPT = sp.io.import_script_from_url("file:contracts/Comptroller.py") +CToken = sp.io.import_script_from_url("file:contracts/CToken.py") +IRM = sp.io.import_script_from_url( + "file:contracts/tests/mock/InterestRateModelMock.py") + + +class CapTestComptroller(CMPT.Comptroller): + def __init__(self, administrator_): + CMPT.Comptroller.__init__( + self, + administrator_=administrator_, + oracleAddress_=sp.address("KT10"), + closeFactorMantissa_=sp.nat(0), + liquidationIncentiveMantissa_=sp.nat(0)) + + @sp.entry_point + def setMarketPriceForTest(self, cToken): + sp.set_type(cToken, sp.TAddress) + self.data.markets[cToken].price.mantissa = sp.nat(int(1e18)) + self.data.markets[cToken].updateLevel = sp.level + + @sp.entry_point + def setLiquidityForTest(self, params): + sp.set_type(params, sp.TRecord(account=sp.TAddress, liquidity=sp.TInt)) + self.data.account_liquidity[params.account] = sp.record( + liquidity=params.liquidity, + updateLevel=sp.level, + valid=sp.bool(True)) + + +class CapTestCToken(CToken.CToken): + def __init__(self, comptroller_, interestRateModel_, administrator_, + initialExchangeRateMantissa_): + CToken.CToken.__init__( + self, + comptroller_, + interestRateModel_, + initialExchangeRateMantissa_, + administrator_, + sp.big_map({ + "": sp.utils.bytes_of_string("tezos-storage:data"), + "data": sp.utils.bytes_of_string(json.dumps({"name": "cap test"})) + }), + { + "name": sp.utils.bytes_of_string("Cap test token"), + "symbol": sp.utils.bytes_of_string("CAP"), + "decimals": sp.utils.bytes_of_string("18") + }, + cash=sp.nat(0)) + + def getCashImpl(self): + return self.data.cash + + def doTransferIn(self, from_, amount): + self.data.cash += amount + + def doTransferOut(self, to_, amount, isContract=False): + self.data.cash = sp.as_nat(self.data.cash - amount) + + +@sp.add_test(name="Cap_Post_State_Boundaries") +def test(): + scenario = sp.test_scenario() + scenario.add_flag("protocol", "lima") + + admin = sp.test_account("cap admin") + supplier = sp.test_account("cap supplier") + borrower = sp.test_account("cap borrower") + + irm = IRM.InterestRateModelMock( + borrowRate_=sp.nat(0), supplyRate_=sp.nat(0)) + scenario += irm + comptroller = CapTestComptroller(administrator_=admin.address) + scenario += comptroller + cToken = CapTestCToken( + comptroller_=comptroller.address, + interestRateModel_=irm.address, + administrator_=admin.address, + initialExchangeRateMantissa_=sp.nat(int(1e18))) + scenario += cToken + + scenario += comptroller.supportMarket(sp.record( + cToken=cToken.address, name="CAP", priceExp=sp.nat(1))).run( + sender=admin, level=1) + scenario += comptroller.setMintPaused(sp.record( + cToken=cToken.address, state=False)).run(sender=admin, level=1) + scenario += comptroller.setBorrowPaused(sp.record( + cToken=cToken.address, state=False)).run(sender=admin, level=1) + scenario += comptroller.setMarketCaps(sp.record( + cToken=cToken.address, supplyCap=sp.nat(200), + borrowCap=sp.nat(100))).run(sender=admin, level=1) + scenario += cToken.accrueInterest().run(sender=supplier, level=1) + + scenario.h2("Supply cap uses the post-mint total exactly once") + scenario += cToken.mint(sp.nat(80)).run(sender=supplier, level=1) + scenario += cToken.mint(sp.nat(120)).run(sender=supplier, level=1) + scenario.verify(cToken.data.totalSupply == sp.nat(200)) + scenario += cToken.mint(sp.nat(1)).run( + sender=supplier, level=1, valid=False, + exception=CMPT.EC.CMPT_SUPPLY_CAP_EXCEEDED) + scenario.verify(cToken.data.totalSupply == sp.nat(200)) + + scenario.h2("Borrow cap uses the post-borrow total exactly once") + scenario += comptroller.setMarketPriceForTest(cToken.address).run(level=1) + scenario += comptroller.setLiquidityForTest(sp.record( + account=borrower.address, liquidity=sp.int(10**30))).run(level=1) + scenario += cToken.borrow(sp.nat(40)).run(sender=borrower, level=1) + scenario += comptroller.setLiquidityForTest(sp.record( + account=borrower.address, liquidity=sp.int(10**30))).run(level=1) + scenario += cToken.borrow(sp.nat(60)).run(sender=borrower, level=1) + scenario.verify(cToken.data.totalBorrows == sp.nat(100)) + scenario += comptroller.setLiquidityForTest(sp.record( + account=borrower.address, liquidity=sp.int(10**30))).run(level=1) + scenario += cToken.borrow(sp.nat(1)).run( + sender=borrower, level=1, valid=False, + exception=CMPT.EC.CMPT_BORROW_CAP_EXCEEDED) + scenario.verify(cToken.data.totalBorrows == sp.nat(100)) + + +@sp.add_test(name="Supply_Cap_Exchange_Rate_Rounding") +def exchange_rate_rounding_test(): + scenario = sp.test_scenario() + scenario.add_flag("protocol", "lima") + + admin = sp.test_account("rounding admin") + supplier = sp.test_account("rounding supplier") + irm = IRM.InterestRateModelMock( + borrowRate_=sp.nat(0), supplyRate_=sp.nat(0)) + scenario += irm + comptroller = CapTestComptroller(administrator_=admin.address) + scenario += comptroller + cToken = CapTestCToken( + comptroller_=comptroller.address, + interestRateModel_=irm.address, + administrator_=admin.address, + initialExchangeRateMantissa_=sp.nat(1500000000000000000)) + scenario += cToken + + scenario += comptroller.supportMarket(sp.record( + cToken=cToken.address, name="ROUND", priceExp=sp.nat(1))).run( + sender=admin, level=1) + scenario += comptroller.setMintPaused(sp.record( + cToken=cToken.address, state=False)).run(sender=admin, level=1) + scenario += comptroller.setMarketCaps(sp.record( + cToken=cToken.address, supplyCap=sp.nat(2), + borrowCap=sp.nat(0))).run(sender=admin, level=1) + scenario += cToken.accrueInterest().run(sender=supplier, level=1) + + scenario += cToken.mint(sp.nat(2)).run(sender=supplier, level=1) + scenario.verify(cToken.data.totalSupply == sp.nat(1)) + scenario.verify(cToken.data.cash == sp.nat(2)) + scenario += cToken.mint(sp.nat(2)).run( + sender=supplier, level=1, valid=False, + exception=CMPT.EC.CMPT_SUPPLY_CAP_EXCEEDED) diff --git a/contracts/tests/ComptrollerTest.py b/contracts/tests/ComptrollerTest.py index 2a73755a..22d8d573 100644 --- a/contracts/tests/ComptrollerTest.py +++ b/contracts/tests/ComptrollerTest.py @@ -112,6 +112,9 @@ def test(): mintPaused = sp.bool(True), borrowPaused = sp.bool(True), redeemPaused = sp.bool(False), + liquidatePaused = sp.bool(False), + supplyCap = sp.nat(10**50), + borrowCap = sp.nat(10**50), name = sp.string("m1"), price = sp.record(mantissa=sp.nat(0)), priceExp = 1000000000000000000, @@ -124,6 +127,9 @@ def test(): mintPaused = sp.bool(True), borrowPaused = sp.bool(True), redeemPaused = sp.bool(False), + liquidatePaused = sp.bool(False), + supplyCap = sp.nat(10**50), + borrowCap = sp.nat(10**50), name = sp.string("m4"), price = sp.record(mantissa=sp.nat(0)), priceExp = 1000000000000000000, @@ -131,6 +137,15 @@ def test(): priceTimestamp= sp.timestamp(0))), ] initMarkets(scenario, bLevel, markets, cmpt) + scenario += cmpt.setPriceOracleAndTimeDiff(sp.record( + priceOracle=oracle.address, timeDiff=sp.int(300))).run( + sender=admin, level=bLevel.next()) + scenario += cmpt.setPriceBounds(sp.record( + cToken=listedMarket, minPrice=sp.nat(1), maxPrice=sp.nat(10**50), + maxChangeBps=sp.nat(10000))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.setPriceBounds(sp.record( + cToken=cTokenMock.address, minPrice=sp.nat(1), maxPrice=sp.nat(10**50), + maxChangeBps=sp.nat(10000))).run(sender=admin, level=bLevel.next()) marketsList = [listedMarket, notListedMarket, listedMarketWithoutAccountMembership, cTokenMock.address] scenario.h4("Add Alice and admin to markets") @@ -174,6 +189,10 @@ def test(): TestAdminFunctionality.checkAdminRequirementH4(scenario, "set transfer paused False", bLevel, admin, alice, cmpt.setTransferPaused, sp.bool(False)) scenario.verify(cmpt.data.transferPaused == sp.bool(False)) + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(10**50), borrowCap=sp.nat(10**50))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.setBorrowPaused(sp.record(cToken=listedMarket, state=sp.bool(False))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.setTransferPaused(sp.bool(False)).run(sender=admin, level=bLevel.next()) + scenario.h2("Test allowed functionality") scenario.h3("Mint allowed") minterArgLambda = lambda market : sp.record(cToken=market, minter=alice.address, mintAmount=sp.nat(100)) @@ -187,6 +206,18 @@ def test(): scenario.h4("mint is not paused") scenario += cmpt.setMintPaused(sp.record(cToken = listedMarket, state = sp.bool(False))).run(sender = admin, level = bLevel.next()) scenario += cmpt.mintAllowed(minterArgLambda(listedMarket)).run(sender = alice, level = bLevel.next()) + scenario.h4("supply cap cannot be exceeded") + scenario += cTokenMock1.setMarketTotals(sp.record(supply=sp.nat(100), borrows=sp.nat(0))) + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(99), borrowCap=sp.nat(10**50))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.mintAllowed(minterArgLambda(listedMarket)).run( + sender=alice, level=bLevel.next(), valid=False, + exception=CMPT.EC.CMPT_SUPPLY_CAP_EXCEEDED) + scenario.h4("supply cap can be reached exactly") + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(100), borrowCap=sp.nat(10**50))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.mintAllowed(minterArgLambda(listedMarket)).run( + sender=alice, level=bLevel.next()) + scenario += cTokenMock1.setMarketTotals(sp.record(supply=sp.nat(0), borrows=sp.nat(0))) + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(10**50), borrowCap=sp.nat(10**50))).run(sender=admin, level=bLevel.next()) scenario.h3("Redeem allowed") cmpt.addToLoansExternal(sp.pair(alice.address, sp.set([cTokenMock.address]))) @@ -208,61 +239,73 @@ def test(): scenario += cmpt.redeemAllowed(redeemArgLambda(notListedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("with insufficient liquidity") cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(0), borrowBalance = sp.nat(100), exchangeRateMantissa = exchRate)).run(level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario.show(cmpt.data.collaterals) scenario.show(alice.address) scenario += cmpt.redeemAllowed(redeemArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("without insufficient liquidity") cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(100*1000000000000000000), borrowBalance = sp.nat(0), exchangeRateMantissa = exchRate)).run(level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.redeemAllowed(redeemArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("invalid after price was not updated for 5 blocks") scenario += cmpt.redeemAllowed(redeemArgLambda(listedMarket)).run(sender = alice, level = bLevel.add(5), valid = False) scenario.h3("Borrow allowed") borrowArgLambda = lambda market : sp.record(cToken=market, borrower=alice.address, borrowAmount=sp.nat(100*1000000000000000000)) + scenario.h4("borrow cap cannot be exceeded") + scenario += cTokenMock1.setMarketTotals(sp.record(supply=sp.nat(0), borrows=sp.nat(100))) + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(10**50), borrowCap=sp.nat(99))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run( + sender=alice, level=bLevel.next(), valid=False, + exception=CMPT.EC.CMPT_BORROW_CAP_EXCEEDED) + scenario += cTokenMock1.setMarketTotals(sp.record(supply=sp.nat(0), borrows=sp.nat(0))) + scenario += cmpt.setMarketCaps(sp.record(cToken=listedMarket, supplyCap=sp.nat(10**50), borrowCap=sp.nat(10**50))).run(sender=admin, level=bLevel.next()) + scenario.h4("borrow completion is internal only") + scenario += cmpt.completeBorrowAllowed(borrowArgLambda(listedMarket)).run( + sender=alice, level=bLevel.next(), valid=False, + exception=CMPT.EC.CMPT_INVALID_BORROW_SENDER) scenario.h4("on the listed market, without updated price") scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.next(), valid = False) scenario.h4("on the listed market, with updated price, without updated liquidity") updateAssetsPrices(scenario, cmpt, bLevel, marketsList) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.next(), valid = False) scenario.h4("on the listed market, with updated price and updated liquidity") - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = True) scenario.h4("borrowing uses the full debt value when the market collateral factor is zero") scenario += cmpt.setCollateralFactor(sp.record(cToken = listedMarket, newCollateralFactor = sp.nat(0))).run(sender = admin, level = bLevel.next()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(sp.record(cToken = listedMarket, borrower = alice.address, borrowAmount = sp.nat(100*1000000000000000000 + 1))).run( sender = alice, level = bLevel.current(), valid = False) scenario.h4("on the not listed market") scenario += cmpt.borrowAllowed(borrowArgLambda(notListedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("with insufficient liquidity") cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(0), borrowBalance = sp.nat(100), exchangeRateMantissa = exchRate)).run(level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("without insufficient liquidity") cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(100*1000000000000000000), borrowBalance = sp.nat(0), exchangeRateMantissa = exchRate)).run(level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("with price errors") oracle.setPrice(0) - updateAssetsPrices(scenario, cmpt, bLevel, marketsList) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), valid = False) - scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) + scenario += cmpt.updateAllAssetPricesWithView().run( + level=bLevel.next(), now=sp.timestamp(100), valid=False, + exception="ASSET_PRICE_OUT_OF_BOUNDS") scenario.h4("without price errors") oracle.setPrice(1) updateAssetsPrices(scenario, cmpt, bLevel, marketsList) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("borrow is paused") scenario += cmpt.setBorrowPaused(sp.record(cToken = listedMarket, state = sp.bool(True))).run(sender = admin, level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("borrow is not paused") scenario += cmpt.setBorrowPaused(sp.record(cToken = listedMarket, state = sp.bool(False))).run(sender = admin, level = bLevel.current()) scenario += cmpt.borrowAllowed(borrowArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("alice calls borrowAllowed if borrower not in market") - scenario += cmpt.updateAccountLiquidityWithView(notMember.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(notMember.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.borrowAllowed(sp.record(cToken=listedMarket, borrower=notMember.address, borrowAmount=sp.nat(0))).run( sender = alice, level = bLevel.current(), valid = False) scenario.h4("cToken calls borrowAllowed if borrower not in market") @@ -278,6 +321,19 @@ def test(): scenario.h4("on the not listed market") scenario += cmpt.repayBorrowAllowed(repayBorrowArgLambda(notListedMarket)).run(sender = alice, level = bLevel.next(), valid = False) + scenario.h3("Liquidation pause does not block repayment") + liquidateArg = sp.record(cTokenBorrowed=listedMarket, + cTokenCollateral=cTokenMock.address, + borrower=alice.address, liquidator=bob.address, + repayAmount=sp.nat(1)) + scenario += cmpt.setLiquidatePaused(sp.record(cToken=listedMarket, state=sp.bool(True))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.liquidateBorrowAllowed(liquidateArg).run( + sender=listedMarket, level=bLevel.next(), valid=False, + exception=CMPT.EC.CMPT_LIQUIDATE_PAUSED) + scenario += cmpt.repayBorrowAllowed(repayBorrowArgLambda(listedMarket)).run( + sender=alice, level=bLevel.next()) + scenario += cmpt.setLiquidatePaused(sp.record(cToken=listedMarket, state=sp.bool(False))).run(sender=admin, level=bLevel.next()) + scenario.h3("Incident mode keeps repayment available") scenario += cmpt.setMintPaused(sp.record(cToken = listedMarket, state = sp.bool(True))).run(sender = admin, level = bLevel.next()) scenario += cmpt.setBorrowPaused(sp.record(cToken = listedMarket, state = sp.bool(True))).run(sender = admin, level = bLevel.current()) @@ -304,13 +360,13 @@ def test(): updateAssetsPrices(scenario, cmpt, bLevel, marketsList) scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.next(), valid = False) scenario.h4("redeem is allowed, with updated price and updated liquidity") - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("redeem is not allowed") scenario += cmpt.transferAllowed(transferArgLambda(notListedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("transfer is paused") scenario += cmpt.setTransferPaused(sp.bool(True)).run(sender = admin, level = bLevel.current()) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("transfer is not paused") scenario += cmpt.setTransferPaused(sp.bool(False)).run(sender = admin, level = bLevel.current()) @@ -329,12 +385,12 @@ def test(): scenario += cTokenMock1.setSnapshotAvailable(sp.bool(True)).run(level = bLevel.next()) scenario += cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(100), borrowBalance = sp.nat(0), exchangeRateMantissa = exchRate)).run(level = bLevel.current()) updateAssetsPrices(scenario, cmpt, bLevel, marketsList) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.current()) scenario.h4("collateralized borrowers cannot transfer into a one-unit shortfall") scenario += cTokenMock.setAccountSnapshot(sp.record(account = alice.address, cTokenBalance = sp.nat(99), borrowBalance = sp.nat(0), exchangeRateMantissa = exchRate)).run(level = bLevel.next()) updateAssetsPrices(scenario, cmpt, bLevel, marketsList) - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.current(), valid = False) scenario.h4("invalid after price was not updated for 5 blocks") scenario += cmpt.transferAllowed(transferArgLambda(listedMarket)).run(sender = alice, level = bLevel.add(5), valid = False) @@ -347,7 +403,7 @@ def test(): updateAssetsPrices(scenario, cmpt, bLevel, marketsList) cmpt.exitMarket(cTokenMock.address).run(sender = alice, level = bLevel.next(), valid = False) scenario.h4("The sender hasn't borrow balance, asset price was updated and updated liquidity") - scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next()) + scenario += cmpt.updateAccountLiquidityWithView(alice.address).run(sender = alice, level = bLevel.next(), now=sp.timestamp(100)) scenario.verify(cmpt.data.collaterals[alice.address].contains(cTokenMock.address)) # account membership should exist before cmpt.exitMarket(cTokenMock.address).run(sender = alice, level = bLevel.current()) scenario.verify( (~ cmpt.data.collaterals[alice.address].contains(cTokenMock.address))) # account membership must be removed @@ -358,13 +414,67 @@ def test(): scenario.h2("Test updateAssetPrice") scenario.h3("Update price") oracle.setPrice(2) - scenario += cmpt.updateAllAssetPricesWithView().run(sender = bob, level = bLevel.next()) + scenario += cmpt.updateAllAssetPricesWithView().run(sender = bob, level = bLevel.next(), now=sp.timestamp(100)) scenario.verify_equal(cmpt.data.markets[listedMarket].price.mantissa, sp.nat(int(2e18))) scenario.verify_equal(cmpt.data.markets[listedMarket].updateLevel, bLevel.current()) scenario.h3("Try to update price at the same level") oracle.setPrice(1) - scenario += cmpt.updateAllAssetPricesWithView().run(sender = bob, level = bLevel.current()) + scenario += cmpt.updateAllAssetPricesWithView().run(sender = bob, level = bLevel.current(), now=sp.timestamp(100)) scenario.verify_equal(cmpt.data.markets[listedMarket].price.mantissa, sp.nat(int(2e18))) + scenario.h3("Reject a price timestamp from the future") + oracle.setTimestamp(sp.timestamp(101)) + scenario += cmpt.updateAllAssetPricesWithView().run( + sender = bob, level = bLevel.next(), now = sp.timestamp(100), + valid = False, exception = "FUTURE_ASSET_PRICE") + scenario.h3("Reject a zero price timestamp") + oracle.setTimestamp(sp.timestamp(0)) + scenario += cmpt.updateAllAssetPricesWithView().run( + sender = bob, level = bLevel.next(), now = sp.timestamp(100), + valid = False, exception = "INVALID_ASSET_PRICE_TIMESTAMP") + scenario.h3("Reject a price timestamp rollback") + oracle.setTimestamp(sp.timestamp(100)) + scenario += cmpt.updateAllAssetPricesWithView().run( + sender = bob, level = bLevel.next(), now = sp.timestamp(100)) + oracle.setTimestamp(sp.timestamp(99)) + scenario += cmpt.updateAllAssetPricesWithView().run( + sender = bob, level = bLevel.next(), now = sp.timestamp(100), + valid = False, exception = "ASSET_PRICE_TIMESTAMP_ROLLBACK") + oracle.clearTimestamp() + scenario.h3("Reject extreme prices outside configured bounds") + scenario += cmpt.setPriceBounds(sp.record(cToken=listedMarket, + minPrice=sp.nat(100000), maxPrice=sp.nat(10000000), + maxChangeBps=sp.nat(2000))).run(sender=admin, level=bLevel.next()) + oracle.setPrice(1) + scenario += cmpt.updateAssetPricesWithView(sp.set([listedMarket])).run( + sender=bob, level=bLevel.next(), now=sp.timestamp(100), valid=False, + exception="ASSET_PRICE_OUT_OF_BOUNDS") + oracle.setPrice(9000000000000000) + scenario += cmpt.updateAssetPricesWithView(sp.set([listedMarket])).run( + sender=bob, level=bLevel.next(), now=sp.timestamp(100), valid=False, + exception="ASSET_PRICE_OUT_OF_BOUNDS") + scenario += cmpt.setPriceBounds(sp.record(cToken=listedMarket, + minPrice=sp.nat(1), maxPrice=sp.nat(10**50), + maxChangeBps=sp.nat(10000))).run(sender=admin, level=bLevel.next()) + oracle.setPrice(1) + scenario.h3("Unrelated unhealthy market does not block account price updates") + healthyMarketAccount = sp.test_account("healthy market account") + scenario += cmpt.addToLoansExternal(sp.pair( + healthyMarketAccount.address, sp.set([listedMarket]))).run( + level=bLevel.next()) + scenario += cmpt.setPriceBounds(sp.record( + cToken=cTokenMock.address, minPrice=sp.nat(2), maxPrice=sp.nat(3), + maxChangeBps=sp.nat(10000))).run(sender=admin, level=bLevel.next()) + scenario += cmpt.updateAllAssetPricesWithView().run( + sender=bob, level=bLevel.next(), now=sp.timestamp(100), valid=False, + exception="ASSET_PRICE_OUT_OF_BOUNDS") + scenario += cmpt.updateAccountLiquidityWithView( + healthyMarketAccount.address).run( + sender=bob, level=bLevel.next(), now=sp.timestamp(100)) + scenario.verify(cmpt.data.account_liquidity[ + healthyMarketAccount.address].valid) + scenario += cmpt.setPriceBounds(sp.record( + cToken=cTokenMock.address, minPrice=sp.nat(1), maxPrice=sp.nat(10**50), + maxChangeBps=sp.nat(10000))).run(sender=admin, level=bLevel.next()) scenario.h2("Test account liquidity") cmpt.enterMarkets(sp.list([cTokenMock.address])).run(sender = bob, level = bLevel.next()) @@ -386,10 +496,27 @@ def test(): result = sp.view("calculateAccountLiquidityExposed", cmpt.address, liquidityParams, t=sp.TRecord(sumBorrowPlusEffects = sp.TNat,sumCollateral = sp.TNat)).open_some() scenario.verify_equal((result.sumCollateral-result.sumBorrowPlusEffects), -90) + scenario.h3("A borrow-only market is counted once, alongside a collateralised market") + # Covers the loans loop for an account that has collateral: the market that is also + # collateral must not be counted twice, and the borrow-only market must still be + # counted. A per-asset flag that failed to reset between iterations would silently + # drop the borrow-only market's debt and overstate liquidity. + mixedProbe = sp.test_account("Collateral plus borrow-only probe") + cmpt.enterMarkets(sp.list([cTokenMock.address])).run(sender = mixedProbe, level = bLevel.next()) + cmpt.addToLoansExternal(sp.pair(mixedProbe.address, sp.set([cTokenMock.address, listedMarket]))).run(level = bLevel.next()) + cTokenMock.setAccountSnapshot(sp.record(account = mixedProbe.address, cTokenBalance = sp.nat(10), borrowBalance = sp.nat(100), exchangeRateMantissa = exchRate)) + cTokenMock1.setAccountSnapshot(sp.record(account = mixedProbe.address, cTokenBalance = sp.nat(0), borrowBalance = sp.nat(50), exchangeRateMantissa = exchRate)) + updateAssetsPrices(scenario, cmpt, bLevel, marketsList) + result = sp.view("calculateAccountLiquidityExposed", cmpt.address, sp.record(account=mixedProbe.address), t=sp.TRecord(sumBorrowPlusEffects = sp.TNat,sumCollateral = sp.TNat)).open_some() + # Only the collateralised market contributes collateral, and it is counted once. + scenario.verify_equal(result.sumCollateral, 10) + # Both debts count: 100 on the collateralised market plus 50 on the borrow-only market. + scenario.verify_equal(result.sumBorrowPlusEffects, 150) + scenario.h2("Test admin functionality") scenario.h3("Set price oracle") TestAdminFunctionality.checkAdminRequirementH4(scenario, "set price oracle", bLevel, admin, alice, cmpt.setPriceOracleAndTimeDiff, - sp.record(priceOracle=priceOracle, timeDiff=86400)) + sp.record(priceOracle=priceOracle, timeDiff=300)) scenario.verify(cmpt.data.oracleAddress == priceOracle) scenario.h3("Set close factor") @@ -424,6 +551,13 @@ def test(): supportMarketParams) scenario.verify(cmpt.data.markets.contains(newMarket) & cmpt.data.markets[newMarket].isListed) scenario.verify(cmpt.data.marketNameToAddress.contains("market-USD")) + scenario.h4("Zero price exponent") + zeroPriceExpMarket = sp.test_account("[supportMarket] zero price exponent").address + scenario += cmpt.supportMarket(sp.record( + cToken=zeroPriceExpMarket, name=sp.string("zero-exp"), + priceExp=sp.nat(0))).run( + sender=admin, level=bLevel.next(), valid=False, + exception="INVALID_PRICE_EXP") scenario.h4("Already listed market") cmpt.supportMarket(supportMarketParams).run(sender = admin, level = bLevel.next(), valid = False) @@ -477,6 +611,9 @@ def test(): mintPaused = sp.bool(False), borrowPaused = sp.bool(False), redeemPaused = sp.bool(False), + liquidatePaused = sp.bool(False), + supplyCap = sp.nat(10**50), + borrowCap = sp.nat(10**50), name = sp.string("extra1"), price = sp.record(mantissa=sp.nat(int(1e18))), priceExp = sp.nat(int(1e18)), @@ -488,6 +625,9 @@ def test(): mintPaused = sp.bool(False), borrowPaused = sp.bool(False), redeemPaused = sp.bool(False), + liquidatePaused = sp.bool(False), + supplyCap = sp.nat(10**50), + borrowCap = sp.nat(10**50), name = sp.string("extra2"), price = sp.record(mantissa=sp.nat(int(1e18))), priceExp = sp.nat(int(1e18)), @@ -548,7 +688,7 @@ def testPauseFunctionsOnMarkets(scenario, actionText, bLevel, sender, callableOb def updateAssetsPrices(scenario, cmpt, bLevel, markets): bLevel.next() - cmpt.updateAllAssetPricesWithView().run(level = bLevel.current()) + cmpt.updateAllAssetPricesWithView().run(level = bLevel.current(), now=sp.timestamp(100)) @sp.add_test(name = "Comptroller_Collateral_Boundaries") @@ -583,6 +723,9 @@ def collateral_boundary_matrix(): mintPaused=sp.bool(False), borrowPaused=sp.bool(False), redeemPaused=sp.bool(False), + liquidatePaused=sp.bool(False), + supplyCap=sp.nat(10**50), + borrowCap=sp.nat(10**50), name=sp.string("boundary"), price=sp.record(mantissa=sp.nat(price)), priceExp=sp.nat(exchange_scale), diff --git a/contracts/tests/GovernanceTest.py b/contracts/tests/GovernanceTest.py index 9dd21bb8..62697529 100644 --- a/contracts/tests/GovernanceTest.py +++ b/contracts/tests/GovernanceTest.py @@ -210,7 +210,7 @@ def testComptroller(scenario, ctoken, bLevel, alice, admin, governor, cmpt, orac scenario.verify(cmpt.data.administrator == governor.address) scenario.h3("Set price oracle") - arg = sp.record(comptroller = cmpt.address, priceOracle = oracle.address, timeDiff=84600) + arg = sp.record(comptroller = cmpt.address, priceOracle = oracle.address, timeDiff=300) TestAdminFunctionality.checkAdminRequirementH4(scenario, "set price oracle", bLevel, admin, alice, governor.setPriceOracleAndTimeDiff, arg) scenario.verify(cmpt.data.oracleAddress == arg.priceOracle) @@ -229,6 +229,15 @@ def testComptroller(scenario, ctoken, bLevel, alice, admin, governor, cmpt, orac TestAdminFunctionality.checkAdminRequirementH4(scenario, "support market", bLevel, admin, alice, governor.supportMarket, arg) scenario.verify(cmpt.data.markets.contains(arg.market.cToken) & cmpt.data.markets[arg.market.cToken].isListed) + scenario.h3("Set price bounds") + boundsArg = sp.record(comptroller=cmpt.address, bounds=sp.record( + cToken=ctoken.address, minPrice=sp.nat(100000), + maxPrice=sp.nat(10000000), maxChangeBps=sp.nat(2000))) + TestAdminFunctionality.checkAdminRequirementH4(scenario, "set price bounds", bLevel, admin, alice, governor.setPriceBounds, boundsArg) + boundsKey = sp.pair(cmpt.address, ctoken.address) + scenario.verify(oracle.data.priceBounds[boundsKey].minPrice == boundsArg.bounds.minPrice) + scenario.verify(oracle.data.priceBounds[boundsKey].maxPrice == boundsArg.bounds.maxPrice) + scenario.h3("Set collateral factor") arg = sp.record(comptroller = cmpt.address, collateralFactor = sp.record(cToken = ctoken.address, newCollateralFactor = sp.nat(2))) TestAdminFunctionality.checkAdminRequirementH4(scenario, "set collateral factor", bLevel, admin, alice, governor.setCollateralFactor, arg) @@ -247,6 +256,17 @@ def testComptroller(scenario, ctoken, bLevel, alice, admin, governor, cmpt, orac TestAdminFunctionality.checkAdminRequirementH4(scenario, "set redeem paused", bLevel, admin, alice, governor.setRedeemPaused, arg) scenario.verify(cmpt.data.markets[arg.tokenState.cToken].redeemPaused == arg.tokenState.state) + scenario.h3("Set liquidation paused") + TestAdminFunctionality.checkAdminRequirementH4(scenario, "set liquidation paused", bLevel, admin, alice, governor.setLiquidatePaused, arg) + scenario.verify(cmpt.data.markets[arg.tokenState.cToken].liquidatePaused == arg.tokenState.state) + + scenario.h3("Set market caps") + capsArg = sp.record(comptroller=cmpt.address, caps=sp.record( + cToken=ctoken.address, supplyCap=sp.nat(1000), borrowCap=sp.nat(500))) + TestAdminFunctionality.checkAdminRequirementH4(scenario, "set market caps", bLevel, admin, alice, governor.setMarketCaps, capsArg) + scenario.verify(cmpt.data.markets[ctoken.address].supplyCap == capsArg.caps.supplyCap) + scenario.verify(cmpt.data.markets[ctoken.address].borrowCap == capsArg.caps.borrowCap) + scenario.h3("Set transfer paused") arg = sp.record(comptroller = cmpt.address, state = sp.bool(False)) TestAdminFunctionality.checkAdminRequirementH4(scenario, "set transfer paused", bLevel, admin, alice, governor.setTransferPaused, arg) diff --git a/contracts/tests/TezFinOracleTest.py b/contracts/tests/TezFinOracleTest.py index cd6e4927..32925a8a 100644 --- a/contracts/tests/TezFinOracleTest.py +++ b/contracts/tests/TezFinOracleTest.py @@ -26,6 +26,33 @@ def getPrice(self, asset, resp): price = sp.compute(sp.snd(oracle_data)) sp.verify(resp == price, "PRICE_MISTMATCH") + @sp.entry_point + def verifyPrice(self, params): + sp.set_type(params, sp.TRecord(asset=sp.TString, price=sp.TNat, + timestamp=sp.TTimestamp)) + oracle_data = sp.view("getPrice", self.contract, params.asset, + t=sp.TPair(sp.TTimestamp, sp.TNat)).open_some( + "invalid oracle view call") + sp.verify(sp.fst(oracle_data) == params.timestamp, "TIMESTAMP_MISMATCH") + sp.verify(sp.snd(oracle_data) == params.price, "PRICE_MISTMATCH") + + @sp.entry_point + def verifyValidatedPrice(self, params): + sp.set_type(params, sp.TRecord( + cToken=sp.TAddress, asset=sp.TString, + previousPrice=sp.TNat, previousTimestamp=sp.TTimestamp, + expectedPrice=sp.TNat)) + oracle_data = sp.view( + "getValidatedPrice", self.contract, + sp.record(comptroller=sp.self_address, cToken=params.cToken, + requestedAsset=params.asset, + previousPrice=params.previousPrice, + previousTimestamp=params.previousTimestamp), + t=sp.TPair(sp.TTimestamp, sp.TNat)).open_some( + "invalid validated oracle view call") + sp.verify(sp.snd(oracle_data) == params.expectedPrice, + "PRICE_MISTMATCH") + @sp.add_test(name="tezfin_oracle") def test(): @@ -59,6 +86,11 @@ def test(): scenario.h2("Consumer Contract") consumer = View_consumer(tezfinOracle.address) scenario += consumer + market = sp.address("KT10") + tezfinOracle.configureMaxPriceAge(sp.int(300)).run(sender=consumer.address) + tezfinOracle.configurePriceBounds(sp.record( + cToken=market, minPrice=sp.nat(10000), maxPrice=sp.nat(20000), + maxChangeBps=sp.nat(2000))).run(sender=consumer.address) scenario.h3("Verify Price") consumer.getPrice(asset="ETH", resp=13425) consumer.getPrice(asset="BTC", resp=2342354345) @@ -66,12 +98,29 @@ def test(): consumer.getPrice(asset="WTZ", resp=203434) consumer.getPrice(asset="OXTZ", resp=203434) consumer.getPrice(asset="RRXTZ", resp=203434) - consumer.getPrice(asset="USD", resp=1000000) - consumer.getPrice(asset="USD", resp=43000000).run(valid=False) + consumer.verifyPrice(asset="FINUSDT", price=1000000, + timestamp=sp.timestamp(16534534)).run( + now=sp.timestamp(16599999)) + consumer.verifyValidatedPrice( + cToken=market, asset="ETH-USD", previousPrice=sp.nat(0), + previousTimestamp=sp.timestamp(0), expectedPrice=sp.nat(13425)).run( + now=sp.timestamp(16534534)) + consumer.verifyValidatedPrice( + cToken=market, asset="ETH-USD", previousPrice=sp.nat(10000), + previousTimestamp=sp.timestamp(16534534), + expectedPrice=sp.nat(13425)).run( + now=sp.timestamp(16534534), valid=False, + exception="ASSET_PRICE_CHANGE_TOO_LARGE") + consumer.verifyValidatedPrice( + cToken=sp.address("KT11"), asset="ETH-USD", + previousPrice=sp.nat(0), previousTimestamp=sp.timestamp(0), + expectedPrice=sp.nat(13425)).run( + now=sp.timestamp(16534534), valid=False, + exception="PRICE_BOUNDS_NOT_CONFIGURED") + consumer.getPrice(asset="USD", resp=1000000).run(valid=False) consumer.getPrice(asset="XTZ", resp=43000000).run(valid=False) consumer.getPrice(asset="ETH", resp=13425) consumer.getPrice(asset="BTC", resp=2342354345) consumer.getPrice(asset="XTZ", resp=203434) - consumer.getPrice(asset="USD", resp=1000000) - consumer.getPrice(asset="USD", resp=43000000).run(valid=False) + consumer.getPrice(asset="USD", resp=1000000).run(valid=False) consumer.getPrice(asset="XTZ", resp=43000000).run(valid=False) diff --git a/contracts/tests/mock/CTokenMock.py b/contracts/tests/mock/CTokenMock.py index b858fbc7..b27bef32 100644 --- a/contracts/tests/mock/CTokenMock.py +++ b/contracts/tests/mock/CTokenMock.py @@ -6,7 +6,8 @@ class CTokenMock(sp.Contract): def __init__(self, test_account_snapshot_): self.init(test_account_snapshot = test_account_snapshot_, - snapshot_available=sp.bool(True), comptroller=sp.address("KT10")) + snapshot_available=sp.bool(True), comptroller=sp.address("KT10"), + totalSupplyUnderlying=sp.nat(0), totalBorrows=sp.nat(0)) @sp.entry_point def setAccountSnapshot(self, params): @@ -18,6 +19,12 @@ def setSnapshotAvailable(self, params): sp.set_type(params, sp.TBool) self.data.snapshot_available = params + @sp.entry_point + def setMarketTotals(self, params): + sp.set_type(params, sp.TRecord(supply=sp.TNat, borrows=sp.TNat)) + self.data.totalSupplyUnderlying = params.supply + self.data.totalBorrows = params.borrows + @sp.utils.view(CTI.TAccountSnapshot) def getAccountSnapshot(self, account): sp.set_type(account, sp.TAddress) @@ -35,6 +42,12 @@ def getAccountSnapshotView(self, account): def accrueInterest(self, params): sp.set_type(params, sp.TUnit) + @sp.onchain_view() + def marketTotals(self, params): + sp.set_type(params, sp.TUnit) + sp.result(sp.pair(self.data.totalSupplyUnderlying, + self.data.totalBorrows)) + @sp.entry_point def setComptroller(self, params): sp.set_type(params, sp.TAddress) diff --git a/contracts/tests/mock/OracleMock.py b/contracts/tests/mock/OracleMock.py index 76d99b5c..88e06608 100644 --- a/contracts/tests/mock/OracleMock.py +++ b/contracts/tests/mock/OracleMock.py @@ -4,12 +4,49 @@ class OracleMock(OracleInterface.OracleInterface): def __init__(self): - self.init(price = sp.nat(0)) + self.init( + price=sp.nat(0), + timestamp=sp.none, + priceBounds=sp.big_map( + l={}, tkey=sp.TPair(sp.TAddress, sp.TAddress), + tvalue=OracleInterface.TPriceBounds), + maxPriceAge=sp.big_map(l={}, tkey=sp.TAddress, tvalue=sp.TInt)) @sp.entry_point def setPrice(self, price): sp.set_type(price, sp.TNat) self.data.price = price + + @sp.entry_point + def setTimestamp(self, timestamp): + sp.set_type(timestamp, sp.TTimestamp) + self.data.timestamp = sp.some(timestamp) + + @sp.entry_point + def clearTimestamp(self): + self.data.timestamp = sp.none + + @sp.entry_point + def configurePriceBounds(self, params): + sp.set_type(params, OracleInterface.TPriceBounds) + sp.verify((params.minPrice > 0) & + (params.minPrice <= params.maxPrice) & + (params.maxChangeBps <= 10000), + "INVALID_PRICE_BOUNDS") + self.data.priceBounds[sp.pair(sp.sender, params.cToken)] = params + + @sp.entry_point + def configureMaxPriceAge(self, maxPriceAge): + sp.set_type(maxPriceAge, sp.TInt) + sp.verify((maxPriceAge > 0) & (maxPriceAge <= 3600), + "INVALID_MAX_PRICE_TIME_DIFFERENCE") + self.data.maxPriceAge[sp.sender] = maxPriceAge + + def getTimestamp(self): + timestamp = sp.local("timestamp", sp.now) + sp.if self.data.timestamp.is_some(): + timestamp.value = self.data.timestamp.open_some() + return timestamp.value @sp.entry_point def get(self, requestPair): @@ -19,10 +56,41 @@ def get(self, requestPair): requestedAsset = sp.compute(sp.fst(requestPair)) callback = sp.compute(sp.snd(requestPair)) - callbackParam = (requestedAsset, (sp.timestamp(0), self.data.price)) + callbackParam = (requestedAsset, (self.getTimestamp(), self.data.price)) sp.transfer(callbackParam, sp.mutez(0), callback) @sp.onchain_view() def getPrice(self, assetCode): sp.set_type(assetCode, sp.TString) - sp.result((sp.timestamp(0), self.data.price)) + sp.result((self.getTimestamp(), self.data.price)) + + @sp.onchain_view() + def getValidatedPrice(self, params): + sp.set_type(params, OracleInterface.TValidatedPriceRequest) + configKey = sp.pair(params.comptroller, params.cToken) + sp.verify(self.data.priceBounds.contains(configKey), + "PRICE_BOUNDS_NOT_CONFIGURED") + sp.verify(self.data.maxPriceAge.contains(params.comptroller), + "MAX_PRICE_AGE_NOT_CONFIGURED") + timestamp = self.getTimestamp() + bounds = self.data.priceBounds[configKey] + sp.verify(timestamp > sp.timestamp(0), "INVALID_ASSET_PRICE_TIMESTAMP") + sp.verify(timestamp <= sp.now, "FUTURE_ASSET_PRICE") + sp.verify(sp.now - timestamp <= self.data.maxPriceAge[params.comptroller], + "STALE_ASSET_PRICE") + sp.verify((params.previousTimestamp == sp.timestamp(0)) | + (timestamp >= params.previousTimestamp), + "ASSET_PRICE_TIMESTAMP_ROLLBACK") + sp.verify((self.data.price >= bounds.minPrice) & + (self.data.price <= bounds.maxPrice), + "ASSET_PRICE_OUT_OF_BOUNDS") + sp.if params.previousTimestamp != sp.timestamp(0): + priceChange = sp.local("priceChange", sp.nat(0)) + sp.if self.data.price >= params.previousPrice: + priceChange.value = sp.as_nat(self.data.price - params.previousPrice) + sp.else: + priceChange.value = sp.as_nat(params.previousPrice - self.data.price) + sp.verify(priceChange.value * 10000 <= + params.previousPrice * bounds.maxChangeBps, + "ASSET_PRICE_CHANGE_TOO_LARGE") + sp.result((timestamp, self.data.price)) diff --git a/deploy/compile_targets/CompileCUSDt.py b/deploy/compile_targets/CompileCUSDt.py index ff38dedc..69872102 100644 --- a/deploy/compile_targets/CompileCUSDt.py +++ b/deploy/compile_targets/CompileCUSDt.py @@ -16,16 +16,16 @@ metadata_ = sp.big_map({ "": sp.utils.bytes_of_string("tezos-storage:data"), "data": sp.utils.bytes_of_string(json.dumps({ - "name": "TezFin Interest-Bearing USD Tether", + "name": "TezFin V3.1 Interest-Bearing USD Tether", "description": "Interest-bearing token for USD Tether (USDt) supplied to the TezFin lending protocol.", - "version": "3.0", + "version": "3.1", "authors": ["Tezos Finance Protocol"], "homepage": "https://tezos.finance", "interfaces": ["TZIP-007", "TZIP-016"], })) }), token_metadata_ = { - "name": sp.utils.bytes_of_string("TezFin Interest-Bearing USD Tether"), + "name": sp.utils.bytes_of_string("TezFin V3.1 Interest-Bearing USD Tether"), "symbol": sp.utils.bytes_of_string("\ua730USDt"), "decimals": sp.utils.bytes_of_string("6"), }, diff --git a/deploy/compile_targets/CompileCUSDtz.py b/deploy/compile_targets/CompileCUSDtz.py index a348ea73..94b75951 100644 --- a/deploy/compile_targets/CompileCUSDtz.py +++ b/deploy/compile_targets/CompileCUSDtz.py @@ -16,16 +16,16 @@ metadata_ = sp.big_map({ "": sp.utils.bytes_of_string("tezos-storage:data"), "data": sp.utils.bytes_of_string(json.dumps({ - "name": "TezFin Interest-Bearing USD Tez", + "name": "TezFin V3.1 Interest-Bearing USD Tez", "description": "Interest-bearing token for USD Tez (USDtz) supplied to the TezFin lending protocol.", - "version": "3.0", + "version": "3.1", "authors": ["Tezos Finance Protocol"], "homepage": "https://tezos.finance", "interfaces": ["TZIP-007", "TZIP-016"], })) }), token_metadata_ = { - "name": sp.utils.bytes_of_string("TezFin Interest-Bearing USD Tez"), + "name": sp.utils.bytes_of_string("TezFin V3.1 Interest-Bearing USD Tez"), "symbol": sp.utils.bytes_of_string("\ua730USDtz"), "decimals": sp.utils.bytes_of_string("6"), }, diff --git a/deploy/compile_targets/CompileCXTZ.py b/deploy/compile_targets/CompileCXTZ.py index d7167602..86130b7a 100644 --- a/deploy/compile_targets/CompileCXTZ.py +++ b/deploy/compile_targets/CompileCXTZ.py @@ -15,16 +15,16 @@ metadata_ = sp.big_map({ "": sp.utils.bytes_of_string("tezos-storage:data"), "data": sp.utils.bytes_of_string(json.dumps({ - "name": "TezFin Interest-Bearing XTZ", + "name": "TezFin V3.1 Interest-Bearing XTZ", "description": "Interest-bearing token for Tez (XTZ) supplied to the TezFin lending protocol.", - "version": "3.0", + "version": "3.1", "authors": ["Tezos Finance Protocol"], "homepage": "https://tezos.finance", "interfaces": ["TZIP-007", "TZIP-016"], })) }), token_metadata_ = { - "name": sp.utils.bytes_of_string("TezFin Interest-Bearing XTZ"), + "name": sp.utils.bytes_of_string("TezFin V3.1 Interest-Bearing XTZ"), "symbol": sp.utils.bytes_of_string("\ua730XTZ"), "decimals": sp.utils.bytes_of_string("6"), } diff --git a/deploy/compile_targets/CompileCstXTZ.py b/deploy/compile_targets/CompileCstXTZ.py index 430751a1..581312fd 100644 --- a/deploy/compile_targets/CompileCstXTZ.py +++ b/deploy/compile_targets/CompileCstXTZ.py @@ -16,16 +16,16 @@ metadata_ = sp.big_map({ "": sp.utils.bytes_of_string("tezos-storage:data"), "data": sp.utils.bytes_of_string(json.dumps({ - "name": "TezFin Interest-Bearing stXTZ", + "name": "TezFin V3.1 Interest-Bearing stXTZ", "description": "Interest-bearing token for stXTZ supplied to the TezFin lending protocol", - "version": "3.0", + "version": "3.1", "authors": ["Tezos Finance Protocol"], "homepage": "https://tezos.finance", "interfaces": ["TZIP-007", "TZIP-016"], })) }), token_metadata_ = { - "name": sp.utils.bytes_of_string("TezFin Interest-Bearing stXTZ"), + "name": sp.utils.bytes_of_string("TezFin V3.1 Interest-Bearing stXTZ"), "symbol": sp.utils.bytes_of_string("\ua730stXTZ"), "decimals": sp.utils.bytes_of_string("6"), }, diff --git a/deploy/compile_targets/CompileIRMs.py b/deploy/compile_targets/CompileIRMs.py index 55bcba3c..d1be7537 100644 --- a/deploy/compile_targets/CompileIRMs.py +++ b/deploy/compile_targets/CompileIRMs.py @@ -4,16 +4,22 @@ CFG = sp.io.import_script_from_url("file:deploy/compile_targets/Config.py") sp.add_compilation_target("CFA12_IRM", IRM.InterestRateModel( - scale_ = 1000000000000000000, - multiplierPerBlock_ = CFG.CFA12_IRM.multiplierPerBlock, - baseRatePerBlock_ = CFG.CFA12_IRM.baseRatePerBlock)) + scale_ = CFG.CUSDtz_IRM.scale, + multiplierPerBlock_ = CFG.CUSDtz_IRM.multiplierPerBlock, + baseRatePerBlock_ = CFG.CUSDtz_IRM.baseRatePerBlock, + jumpMultiplierPerBlock_ = CFG.CUSDtz_IRM.jumpMultiplierPerBlock, + kink_ = CFG.CUSDtz_IRM.kink)) sp.add_compilation_target("CFA2_IRM", IRM.InterestRateModel( - scale_ = 1000000000000000000, - multiplierPerBlock_ = CFG.CFA2_IRM.multiplierPerBlock, - baseRatePerBlock_ = CFG.CFA2_IRM.baseRatePerBlock)) + scale_ = CFG.CUSDt_IRM.scale, + multiplierPerBlock_ = CFG.CUSDt_IRM.multiplierPerBlock, + baseRatePerBlock_ = CFG.CUSDt_IRM.baseRatePerBlock, + jumpMultiplierPerBlock_ = CFG.CUSDt_IRM.jumpMultiplierPerBlock, + kink_ = CFG.CUSDt_IRM.kink)) sp.add_compilation_target("CXTZ_IRM", IRM.InterestRateModel( - scale_ = 1000000000000000000, + scale_ = CFG.CXTZ_IRM.scale, multiplierPerBlock_ = CFG.CXTZ_IRM.multiplierPerBlock, - baseRatePerBlock_ = CFG.CXTZ_IRM.baseRatePerBlock)) + baseRatePerBlock_ = CFG.CXTZ_IRM.baseRatePerBlock, + jumpMultiplierPerBlock_ = CFG.CXTZ_IRM.jumpMultiplierPerBlock, + kink_ = CFG.CXTZ_IRM.kink)) diff --git a/deploy/compile_targets/CompileTestData.py b/deploy/compile_targets/CompileTestData.py index 84aaa417..9e9cf8af 100644 --- a/deploy/compile_targets/CompileTestData.py +++ b/deploy/compile_targets/CompileTestData.py @@ -2,15 +2,17 @@ UTILS = sp.io.import_script_from_url("file:deploy/compile_targets/Utils.py") CFG = sp.io.import_script_from_url("file:deploy/compile_targets/Config.py") -# use harbinger instead of mock -# Oracle = sp.io.import_script_from_url("file:deploy/test_data/PriceOracle.py") +# Previewnet-only mock oracle. Do NOT use this for mainnet: mainnet must use the +# vetted production oracle address supplied via the mainnet manifest/profile instead +# of compiling/originating this mock. See README.md "Deployment Manifest" section. +Oracle = sp.io.import_script_from_url("file:deploy/test_data/PriceOracle.py") FA12 = sp.io.import_script_from_url("file:deploy/test_data/FA1.2.py") FA2 = sp.io.import_script_from_url("file:deploy/test_data/FA2.py") UTILS.checkDependencies(CFG.FA12) UTILS.checkDependencies(CFG.FA2) -# Oracle.compile() -FA12.compile("tzBTC",CFG.deployResult.OriginatorAddress) -FA12.compile("USDtz",CFG.deployResult.OriginatorAddress) -FA2.compile("USDt",CFG.deployResult.OriginatorAddress) \ No newline at end of file +Oracle.compile() +FA12.compile("tzBTC", CFG.deployResult.OriginatorAddress) +FA12.compile("USDtz", CFG.deployResult.OriginatorAddress) +FA2.compile("USDt", CFG.deployResult.OriginatorAddress) diff --git a/deploy/compile_targets/CompileTezFinOracle.py b/deploy/compile_targets/CompileTezFinOracle.py index 71ef4343..2d79a04c 100644 --- a/deploy/compile_targets/CompileTezFinOracle.py +++ b/deploy/compile_targets/CompileTezFinOracle.py @@ -3,6 +3,13 @@ TezFinOracle = sp.io.import_script_from_url( "file:contracts/TezFinOracle.py").TezFinOracle CFG = sp.io.import_script_from_url("file:deploy/compile_targets/Config.py") +UTILS = sp.io.import_script_from_url("file:deploy/compile_targets/Utils.py") + +# Fails early and explicitly (instead of a bare AttributeError) if PriceOracle hasn't +# been compiled/deployed yet for this network profile. See CompileTestData.py (Previewnet +# mock) and README.md ("Deployment Manifest" / PriceOracle) for how PriceOracle should be +# supplied for each network. +UTILS.checkDependencies(CFG.TezFinOracle) sp.add_compilation_target("TezFinOracle", TezFinOracle( admin=sp.address(CFG.deployResult.OriginatorAddress), diff --git a/deploy/compile_targets/CompileTzBTC.py b/deploy/compile_targets/CompileTzBTC.py index f6ce3f9b..f579c564 100644 --- a/deploy/compile_targets/CompileTzBTC.py +++ b/deploy/compile_targets/CompileTzBTC.py @@ -1,31 +1,36 @@ import smartpy as sp import json +from types import SimpleNamespace CFG = sp.io.import_script_from_url("file:deploy/compile_targets/Config.py") CFA12 = sp.io.import_script_from_url("file:contracts/CFA12.py") UTILS = sp.io.import_script_from_url("file:deploy/compile_targets/Utils.py") -UTILS.checkDependencies(CFG.CFA12) +# tzBTC uses its own dedicated CtzBTC_IRM (see CompileCtzBTC_IRM.py / Config.json +# "CtzBTC_IRM"), not the CFA12_IRM used by CUSDtz. Check that dependency explicitly +# here instead of reusing CFG.CFA12.dependencies, which is shared with +# CompileCUSDtz.py and still correctly points at CFA12_IRM for that market. +UTILS.checkDependencies(SimpleNamespace(dependencies=["Governance", "tzBTC", "CtzBTC_IRM"])) sp.add_compilation_target("CtzBTC", CFA12.CFA12( comptroller_ = sp.address(CFG.deployResult.Comptroller), - interestRateModel_ = sp.address(CFG.deployResult.CFA12_IRM), - initialExchangeRateMantissa_ = sp.nat(CFG.CFA2.initialExchangeRateMantissa), + interestRateModel_ = sp.address(CFG.deployResult.CtzBTC_IRM), + initialExchangeRateMantissa_ = sp.nat(CFG.CFA12.initialExchangeRateMantissa), administrator_ = sp.address(CFG.deployResult.Governance), # specify metadata before deployment metadata_ = sp.big_map({ "": sp.utils.bytes_of_string("tezos-storage:data"), "data": sp.utils.bytes_of_string(json.dumps({ - "name": "TezFin Interest-Bearing tzBTC", + "name": "TezFin V3.1 Interest-Bearing tzBTC", "description": "Interest-bearing token for tzBTC (wrapped Bitcoin on Tezos) supplied to the TezFin lending protocol.", - "version": "3.0", + "version": "3.1", "authors": ["Tezos Finance Protocol"], "homepage": "https://tezos.finance", "interfaces": ["TZIP-007", "TZIP-016"], })) }), token_metadata_ = { - "name": sp.utils.bytes_of_string("TezFin Interest-Bearing tzBTC"), + "name": sp.utils.bytes_of_string("TezFin V3.1 Interest-Bearing tzBTC"), "symbol": sp.utils.bytes_of_string("\ua730tzBTC"), "decimals": sp.utils.bytes_of_string("8"), }, diff --git a/deploy/compile_targets/Config.json b/deploy/compile_targets/Config.json index ae4ab668..2d5081fa 100644 --- a/deploy/compile_targets/Config.json +++ b/deploy/compile_targets/Config.json @@ -6,14 +6,14 @@ "jumpMultiplierPerBlock": 875200000000, "kink": 700000000000000000 }, - "CUSDt_IRM" :{ + "CUSDt_IRM": { "scale": 1000000000000000000, "multiplierPerBlock": 12970000000, "baseRatePerBlock": 0, "jumpMultiplierPerBlock": 697500000000, "kink": 880000000000000000 }, - "CUSDtz_IRM" :{ + "CUSDtz_IRM": { "scale": 1000000000000000000, "multiplierPerBlock": 12970000000, "baseRatePerBlock": 0, @@ -44,7 +44,7 @@ "liquidationIncentiveMantissa": 1050000000000000000, "dependencies": [ "Governance", - "PriceOracle" + "TezFinOracle" ] }, "CFA2": { @@ -80,5 +80,11 @@ "dependencies": [ "OriginatorAddress" ] + }, + "TezFinOracle": { + "dependencies": [ + "OriginatorAddress", + "PriceOracle" + ] } } diff --git a/deploy/compile_targets/Config.py b/deploy/compile_targets/Config.py index 1f729ef7..3a61dc62 100644 --- a/deploy/compile_targets/Config.py +++ b/deploy/compile_targets/Config.py @@ -3,7 +3,9 @@ from types import SimpleNamespace PATH_COMPILE_CONFIG = "deploy/compile_targets/Config.json" +PATH_DEPLOY_SCRIPT_CONFIG = "deploy/deploy_script/config.json" PATH_DEPLOY_RESULT = "TezFinBuild/deploy_result/deploy.json" +PATH_DEPLOY_RESULT_MAINNET = "TezFinBuild/deploy_result/deploy.mainnet.json" class JsonDeserializer: # order to formulate correct path, execution must be started from root directory "TezFin" @@ -16,8 +18,22 @@ def Deserialize(relativePath): return json.loads(file.read(), object_hook=lambda d: SimpleNamespace(**d)) compileConfig = JsonDeserializer.Deserialize(PATH_COMPILE_CONFIG) +deployScriptConfig = JsonDeserializer.Deserialize(PATH_DEPLOY_SCRIPT_CONFIG) + +# Manifest path resolution must match resolveDeployResultPath() in deploy_script/util.js +# exactly, otherwise prepare/deploy (JS) and compile targets (this file) can silently +# read/write different manifests for the same run: +# 1. DEPLOY_MANIFEST env var, if set (explicit override always wins). +# 2. A profile-specific default derived from deploy_script/config.json's +# networkProfile, so a mainnet config.json never defaults to the Previewnet +# manifest file (or vice versa). +_defaultDeployResultPath = ( + PATH_DEPLOY_RESULT_MAINNET + if getattr(deployScriptConfig, 'networkProfile', None) == 'mainnet' + else PATH_DEPLOY_RESULT +) deployResult = JsonDeserializer.Deserialize( - os.getenv('E2E', PATH_DEPLOY_RESULT)) + os.getenv('DEPLOY_MANIFEST', os.getenv('E2E', _defaultDeployResultPath))) CUSDt_IRM = compileConfig.CUSDt_IRM CUSDtz_IRM = compileConfig.CUSDtz_IRM @@ -27,6 +43,7 @@ def Deserialize(relativePath): Governance = compileConfig.Governance Comptroller = compileConfig.Comptroller +TezFinOracle = compileConfig.TezFinOracle CFA2 = compileConfig.CFA2 CFA12 = compileConfig.CFA12 CXTZ = compileConfig.CXTZ diff --git a/deploy/compile_targets/Utils.py b/deploy/compile_targets/Utils.py index d4e8c254..de4641c3 100644 --- a/deploy/compile_targets/Utils.py +++ b/deploy/compile_targets/Utils.py @@ -1,4 +1,5 @@ import smartpy as sp +import os CFG = sp.io.import_script_from_url("file:deploy/compile_targets/Config.py") @@ -9,4 +10,11 @@ def checkDependencies(obj): notSpecifiedAttributes.append(attribute) if len(notSpecifiedAttributes) > 0: - raise Exception(f'Please specify {notSpecifiedAttributes} in {CFG.PATH_DEPLOY_RESULT}') + manifest = os.getenv('DEPLOY_MANIFEST', os.getenv('E2E', CFG._defaultDeployResultPath)) + message = f'Please specify {notSpecifiedAttributes} in the deploy manifest ({manifest}).' + if 'PriceOracle' in notSpecifiedAttributes: + message += ( + ' For Previewnet, deploy the mock PriceOracle via CompileTestData.py first. ' + 'For mainnet, put the vetted production oracle address under PriceOracle before compiling.' + ) + raise Exception(message) diff --git a/deploy/compile_targets/tests/test_deploy_pipeline_wiring.py b/deploy/compile_targets/tests/test_deploy_pipeline_wiring.py new file mode 100644 index 00000000..09f5e0f6 --- /dev/null +++ b/deploy/compile_targets/tests/test_deploy_pipeline_wiring.py @@ -0,0 +1,228 @@ +"""Cheap, static (no SmartPy, no network) sanity checks for the deployment pipeline +wiring itself, as opposed to the compiled contracts. These catch a class of bug where +a shell script or config references a file/attribute that doesn't actually exist, or +where the JS and Python sides of manifest-path resolution silently disagree - the kind +of mistake that's easy to introduce when editing deploy_previewnet.sh/deploy_mainnet.sh +or Config.py/util.js without re-running an actual deployment. + +Covers: + 1. Every `Compile*.py` target invoked from deploy_previewnet.sh/deploy_mainnet.sh + actually exists on disk (catches a typo'd/renamed/deleted compile target being + left dangling in a shell script). + 2. CompileCtzBTC_IRM.py reads its parameters from CFG.CtzBTC_IRM (its own, + asset-specific IRM config block), not another market's IRM config block by + mistake - mirrors the same class of bug test_irm_wiring.py catches for the + ꜰToken market compile targets themselves. + 3. Config.py's `_defaultDeployResultPath` and deploy_script/util.js's + `resolveDeployResultPath()` resolve to the same default manifest file for a given + `networkProfile` ("previewnet" vs "mainnet"), so the two languages can't silently + read/write different manifests for the same run (this parity is already asserted + in comments in both files; this test actually executes both and compares). + +Run with: python3 deploy/compile_targets/tests/test_deploy_pipeline_wiring.py +""" +import json +import os +import re +import subprocess +import sys +import tempfile + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +COMPILE_TARGETS_DIR = os.path.join(REPO_ROOT, 'deploy', 'compile_targets') +SHELL_SCRIPTS_DIR = os.path.join(REPO_ROOT, 'deploy', 'shell_scripts') +UTIL_JS_PATH = os.path.join(REPO_ROOT, 'deploy', 'deploy_script', 'util.js') + +SHELL_SCRIPTS_TO_CHECK = ['deploy_previewnet.sh', 'deploy_mainnet.sh'] + +COMPILE_TARGET_REFERENCE_PATTERN = re.compile( + r'compile_and_deploy\s+\./deploy/compile_targets/(\S+\.py)' +) + + +def check_shell_script_compile_targets(): + """Every Compile*.py referenced from a deploy shell script must exist on disk.""" + failures = [] + for scriptName in SHELL_SCRIPTS_TO_CHECK: + scriptPath = os.path.join(SHELL_SCRIPTS_DIR, scriptName) + if not os.path.exists(scriptPath): + failures.append(f'{scriptName}: shell script not found at {scriptPath}') + continue + + with open(scriptPath) as f: + source = f.read() + + referencedFiles = COMPILE_TARGET_REFERENCE_PATTERN.findall(source) + if not referencedFiles: + failures.append(f'{scriptName}: no "compile_and_deploy ./deploy/compile_targets/*.py" calls found at all') + continue + + for fileName in referencedFiles: + targetPath = os.path.join(COMPILE_TARGETS_DIR, fileName) + if not os.path.exists(targetPath): + failures.append( + f'{scriptName}: references {fileName}, but no such file exists at ' + f'{targetPath} (typo, renamed, or deleted compile target left dangling in the script).' + ) + return failures + + +def check_ctzbtc_irm_config_source(): + """CompileCtzBTC_IRM.py must read its parameters from CFG.CtzBTC_IRM, not another + market's IRM config block (e.g. CFG.CFA12_IRM) by mistake.""" + filePath = os.path.join(COMPILE_TARGETS_DIR, 'CompileCtzBTC_IRM.py') + if not os.path.exists(filePath): + return [f'CompileCtzBTC_IRM.py not found at {filePath}'] + + with open(filePath) as f: + source = f.read() + + referencedConfigKeys = set(re.findall(r'CFG\.(\w*_IRM)\.', source)) + if referencedConfigKeys != {'CtzBTC_IRM'}: + return [ + f'CompileCtzBTC_IRM.py: expected it to read parameters only from CFG.CtzBTC_IRM, ' + f'but found references to {sorted(referencedConfigKeys) or "no IRM config at all"} instead.' + ] + return [] + + +def resolve_python_default_manifest_path(networkProfile): + """Runs Config.py's actual _defaultDeployResultPath logic (not a re-implementation + of it), by exec'ing its source with JsonDeserializer.Deserialize monkeypatched (via + a runner script written to a temp file, to avoid any nested-quoting issues with + -c) so the deploy_script/config.json read returns a fake object with just the + networkProfile under test, without touching the real (copilot-ignored) config.json + on disk.""" + configPyPath = os.path.join(COMPILE_TARGETS_DIR, 'Config.py') + runnerSource = ( + "import sys, json\n" + "from types import SimpleNamespace\n" + f"sys.path.insert(0, {COMPILE_TARGETS_DIR!r})\n" + f"with open({configPyPath!r}) as f:\n" + " configPySource = f.read()\n" + "patched = configPySource.replace(\n" + " 'deployScriptConfig = JsonDeserializer.Deserialize(PATH_DEPLOY_SCRIPT_CONFIG)',\n" + f" 'deployScriptConfig = SimpleNamespace(networkProfile=' + repr({networkProfile!r}) + ')',\n" + ")\n" + # Only exec up to (and including) the _defaultDeployResultPath assignment: the + # lines after it try to actually open the resolved manifest file, which may not + # exist locally (e.g. deploy.mainnet.json before a real mainnet run has ever + # happened). We only care about the resolved *path*, not reading the manifest. + "marker = 'deployResult = JsonDeserializer.Deserialize('\n" + "cutoff = patched.index(marker)\n" + "truncated = patched[:cutoff]\n" + "namespace = {'__name__': 'Config'}\n" + "exec(compile(truncated, 'Config.py', 'exec'), namespace)\n" + "print(namespace['_defaultDeployResultPath'])\n" + ) + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as runnerFile: + runnerFile.write(runnerSource) + runnerPath = runnerFile.name + try: + result = subprocess.run([sys.executable, runnerPath], cwd=REPO_ROOT, capture_output=True, text=True) + finally: + os.unlink(runnerPath) + if result.returncode != 0: + raise RuntimeError(f'Failed to evaluate Config.py default path for networkProfile={networkProfile!r}: {result.stderr}') + return result.stdout.strip() + + +def resolve_js_default_manifest_path(networkProfile): + """Runs deploy_script/util.js's actual resolveDeployResultPath() (not a + re-implementation of it) from its real location, so Node's normal module + resolution finds deploy_script/node_modules (e.g. `glob`) without needing to copy + or symlink node_modules into a temp directory (which breaks in CI when + node_modules hasn't been installed under a symlink target, or when the temp dir + lives on a different filesystem than the repo checkout). + + To inject the networkProfile under test without permanently mutating the real + (copilot-ignored) config.json, this backs up the real file's bytes (if it exists), + overwrites it with a throwaway config declaring the given networkProfile, runs the + check, and restores the original bytes (or removes the file if it didn't exist + before) in a finally block - so a crash mid-test can't leave a stray fake + config.json behind unnoticed and the developer's real config.json is untouched by + the time this function returns either way.""" + realDeployScriptDir = os.path.dirname(UTIL_JS_PATH) + configJsonPath = os.path.join(realDeployScriptDir, 'config.json') + + originalConfigBytes = None + configJsonExisted = os.path.exists(configJsonPath) + if configJsonExisted: + with open(configJsonPath, 'rb') as f: + originalConfigBytes = f.read() + + try: + with open(configJsonPath, 'w') as f: + json.dump({'networkProfile': networkProfile, 'tezosNode': 'https://example.invalid'}, f) + + script = ( + f"const {{ resolveDeployResultPath }} = require({json.dumps(UTIL_JS_PATH)});" + "console.log(resolveDeployResultPath());" + ) + env = os.environ.copy() + env.pop('DEPLOY_MANIFEST', None) + result = subprocess.run( + ['node', '-e', script], + cwd=REPO_ROOT, + capture_output=True, + text=True, + env=env, + ) + if result.returncode != 0: + raise RuntimeError(f'Failed to evaluate util.js default path for networkProfile={networkProfile!r}: {result.stderr}') + return result.stdout.strip() + finally: + if configJsonExisted: + with open(configJsonPath, 'wb') as f: + f.write(originalConfigBytes) + else: + try: + os.remove(configJsonPath) + except FileNotFoundError: + pass + + +def check_manifest_path_resolution_parity(): + """Config.py (Python) and util.js (JS) must resolve to the same default manifest + file for a given networkProfile, otherwise compile targets and deploy scripts can + silently read/write different files for what's meant to be the same run.""" + failures = [] + for networkProfile in ['previewnet', 'mainnet', None]: + try: + pyPath = resolve_python_default_manifest_path(networkProfile) + jsPath = resolve_js_default_manifest_path(networkProfile) + except RuntimeError as error: + failures.append(str(error)) + continue + + pyFileName = os.path.basename(pyPath) + jsFileName = os.path.basename(jsPath) + if pyFileName != jsFileName: + failures.append( + f'networkProfile={networkProfile!r}: Config.py resolves to "{pyFileName}", but util.js ' + f'resolves to "{jsFileName}". These must match or the JS and Python sides of the deploy ' + f'pipeline can silently disagree about which manifest file to use.' + ) + return failures + + +def main(): + failures = [] + failures += check_shell_script_compile_targets() + failures += check_ctzbtc_irm_config_source() + failures += check_manifest_path_resolution_parity() + + if failures: + print('Deploy pipeline wiring check FAILED:') + for failure in failures: + print(f' - {failure}') + sys.exit(1) + + print('Deploy pipeline wiring check passed:') + print(' - All Compile*.py targets referenced from deploy_previewnet.sh/deploy_mainnet.sh exist.') + print(' - CompileCtzBTC_IRM.py reads parameters only from CFG.CtzBTC_IRM.') + print(' - Config.py and util.js agree on the default manifest path for previewnet/mainnet/unset profiles.') + + +if __name__ == '__main__': + main() diff --git a/deploy/compile_targets/tests/test_irm_wiring.py b/deploy/compile_targets/tests/test_irm_wiring.py new file mode 100644 index 00000000..21fb0147 --- /dev/null +++ b/deploy/compile_targets/tests/test_irm_wiring.py @@ -0,0 +1,80 @@ +"""Static check that each ꜰToken market's compile target is wired to its own, +asset-specific interest-rate-model config key from Config.json, instead of reusing +another market's IRM config by mistake (as tzBTC previously reused CUSDtz's CFA12_IRM +via CompileIRMs.py, even though a dedicated CtzBTC_IRM config/compile target existed). + +This test inspects only the deploy_result._IRM attribute lookups performed by +each CompileXxx.py compile target (by reading the source), and confirms that they +match this expected mapping instead of trying to execute the SmartPy compile targets +directly (which requires a populated deploy manifest and the SmartPy runtime). + +Run with: python3 deploy/compile_targets/tests/test_irm_wiring.py +""" +import os +import re +import sys + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +COMPILE_TARGETS_DIR = os.path.join(REPO_ROOT, 'deploy', 'compile_targets') + +# Expected mapping: compile target file -> (market name, expected IRM manifest key). +# Add new markets here as they are added to the deploy pipeline. +EXPECTED_MARKET_IRM = { + 'CompileCUSDt.py': ('CUSDt', 'CFA2_IRM'), + 'CompileCUSDtz.py': ('CUSDtz', 'CFA12_IRM'), + 'CompileCXTZ.py': ('CXTZ', 'CXTZ_IRM'), + 'CompileTzBTC.py': ('CtzBTC', 'CtzBTC_IRM'), +} + +IRM_ATTR_PATTERN = re.compile(r'CFG\.deployResult\.(\w*_IRM)') + + +def find_irm_reference(sourceText): + matches = set(IRM_ATTR_PATTERN.findall(sourceText)) + return matches + + +def main(): + failures = [] + + for fileName, (marketName, expectedIrmKey) in EXPECTED_MARKET_IRM.items(): + filePath = os.path.join(COMPILE_TARGETS_DIR, fileName) + if not os.path.exists(filePath): + failures.append(f'{fileName}: file not found at {filePath}') + continue + + with open(filePath) as f: + source = f.read() + + referencedIrmKeys = find_irm_reference(source) + if expectedIrmKey not in referencedIrmKeys: + failures.append( + f'{fileName} ({marketName} market): expected it to reference ' + f'CFG.deployResult.{expectedIrmKey}, but found references to ' + f'{sorted(referencedIrmKeys) or "no IRM at all"} instead.' + ) + elif len(referencedIrmKeys) > 1: + failures.append( + f'{fileName} ({marketName} market): references multiple IRM keys ' + f'{sorted(referencedIrmKeys)}; expected exactly {expectedIrmKey}.' + ) + + if fileName == 'CompileTzBTC.py' and 'CFG.CFA12.initialExchangeRateMantissa' not in source: + failures.append( + 'CompileTzBTC.py (CtzBTC market): expected the FA1.2 initial ' + 'exchange rate from CFG.CFA12.' + ) + + if failures: + print('IRM wiring check FAILED:') + for failure in failures: + print(f' - {failure}') + sys.exit(1) + + print(f'IRM wiring check passed for {len(EXPECTED_MARKET_IRM)} market(s):') + for fileName, (marketName, expectedIrmKey) in EXPECTED_MARKET_IRM.items(): + print(f' - {marketName} ({fileName}) -> {expectedIrmKey}') + + +if __name__ == '__main__': + main() diff --git a/deploy/compile_targets/tests/test_mainnet_governance_payload.py b/deploy/compile_targets/tests/test_mainnet_governance_payload.py new file mode 100644 index 00000000..140f08a0 --- /dev/null +++ b/deploy/compile_targets/tests/test_mainnet_governance_payload.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +import json +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[3] +PAYLOAD_PATH = ROOT / "docs" / "MainnetGovernancePayloads.json" +MARKETS = ("XTZ", "USDT", "TZBTC") +MARKET_ACTIONS = ("mint", "borrow", "redeem", "liquidate") + + +class MainnetGovernancePayloadTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.payload = json.loads(PAYLOAD_PATH.read_text(encoding="utf-8")) + cls.phases = {phase["name"]: phase for phase in cls.payload["phases"]} + + def test_oracle_max_price_age_is_accepted_by_contract(self): + max_price_age = self.payload["requiredInputs"][ + "maxPriceTimeDifferenceSeconds"] + self.assertGreater(max_price_age, 0) + self.assertLessEqual(max_price_age, 3600) + + def test_closed_market_configuration_is_complete_and_ordered(self): + operations = self.phases[ + "multisig_configures_closed_markets"]["operations"] + entrypoints = [operation["entrypoint"] for operation in operations] + + self.assertLess(entrypoints.index("setPriceOracleAndTimeDiff"), + entrypoints.index("supportMarket")) + self.assertLess(entrypoints.index("supportMarket"), + entrypoints.index("setPriceBounds")) + self.assertLess(entrypoints.index("setPriceBounds"), + entrypoints.index("setMarketCaps")) + self.assertLess(entrypoints.index("setMarketCaps"), + entrypoints.index("setCollateralFactor")) + + required_inputs = self.payload["requiredInputs"] + self.assertEqual(set(required_inputs["priceBounds"]), set(MARKETS)) + self.assertEqual(set(required_inputs["marketCaps"]), set(MARKETS)) + + def test_unapproved_manifest_remains_blocked_and_fail_closed(self): + self.assertEqual(self.payload["status"], "INITIAL_DRAFT") + + required_inputs = self.payload["requiredInputs"] + risk_values = [] + for market in MARKETS: + risk_values.extend(required_inputs["priceBounds"][market].values()) + risk_values.extend(required_inputs["marketCaps"][market].values()) + self.assertTrue(risk_values) + self.assertTrue(all(value == "TODO_VALUE" for value in risk_values)) + + activation = required_inputs["activateAfterVerification"] + for market in MARKETS: + for action in MARKET_ACTIONS: + self.assertFalse(activation[market][action]) + self.assertFalse(activation["transfers"]) + + blocking_conditions = self.phases[ + "multisig_verifies_closed_market_configuration"][ + "blockingConditions"] + self.assertIn("No TODO_VALUE values remain", blocking_conditions) + + def test_activation_uses_independent_pause_entrypoints(self): + operations = self.phases[ + "multisig_opens_approved_markets"]["operations"] + self.assertTrue(all("repeatEntrypoints" not in operation + for operation in operations)) + + entrypoints = {operation["entrypoint"] for operation in operations} + self.assertTrue({ + "setMintPaused", + "setBorrowPaused", + "setRedeemPaused", + "setLiquidatePaused", + "setTransferPaused", + }.issubset(entrypoints)) + + def test_activation_is_preceded_by_storage_verification(self): + phase_names = [phase["name"] for phase in self.payload["phases"]] + verification_index = phase_names.index( + "multisig_verifies_closed_market_configuration") + activation_index = phase_names.index( + "multisig_opens_approved_markets") + self.assertLess(verification_index, activation_index) + + checks = self.phases[ + "multisig_verifies_closed_market_configuration"][ + "requiredStorageChecks"] + self.assertTrue(any("priceBounds" in check for check in checks)) + self.assertTrue(any("supplyCap" in check and "borrowCap" in check + for check in checks)) + self.assertTrue(any("liquidation" in check for check in checks)) + + +if __name__ == "__main__": + unittest.main() diff --git a/deploy/compile_targets/tests/test_operation_size.py b/deploy/compile_targets/tests/test_operation_size.py new file mode 100644 index 00000000..a9218f7d --- /dev/null +++ b/deploy/compile_targets/tests/test_operation_size.py @@ -0,0 +1,233 @@ +"""Guards against a contract growing past the origination operation size limit before it +reaches mainnet. This was called out specifically in the PR #455 review re: Comptroller's +`lazify=True` removal - that change alone is benign (SmartPy just emits a +differently-shaped, non-lazified Michelson representation), but the review recommended +verifying the actual origination operation size stays comfortably under protocol limits +before a real mainnet deployment. + +The number that has to fit under `max_operation_data_length` (32768) is the size of the +whole signed manager operation, NOT just the contract code and initial storage. SmartPy's +`*_sizes.csv` only reports the latter, so gating on code+storage under-reports the real +figure by ~140 bytes of operation framing plus a 64-byte signature. For the Comptroller +that difference was the whole apparent safety margin, so this check measures the forged +operation instead, via deploy_script/measure_origination_size.js. That produces the same +quantity Taquito reports as `estimate.originate().opSize`, but it forges locally, so it +needs no RPC, no funded account and no secret key and can run on every commit. The +code+storage figures are still printed for continuity with previous CI output. + +Unlike a naive version of this check, this test does NOT read whatever happens to be +checked in under compiled_contracts/ (which can be stale relative to the current PR's +source changes). Instead it invokes the real SmartPy CLI to compile the relevant +targets fresh, into a temporary output directory, against a throwaway manifest of +placeholder addresses (borrowed from e2e/deploy_result/deploy.json, which already +contains a full set of syntactically-valid KT1/tz1 addresses for exactly this +purpose). This means a source change that meaningfully grows a contract (e.g. +disabling code sharing/lazification) is caught in the same PR that introduces it, +without needing a network connection. + +Requires deploy/deploy_script/node_modules (`npm ci` in that directory) for the Taquito +forger; CI installs it before this step. + +Run with: + python3 deploy/compile_targets/tests/test_operation_size.py [/path/to/SmartPy.sh] +(defaults to ~/smartpy-cli/SmartPy.sh if no argument/env var is given) +""" +import csv +import json +import os +import shutil +import subprocess +import sys +import tempfile + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +PLACEHOLDER_MANIFEST = os.path.join(REPO_ROOT, 'e2e', 'deploy_result', 'deploy.json') +DEPLOY_SCRIPT_DIR = os.path.join(REPO_ROOT, 'deploy', 'deploy_script') +MEASURE_SCRIPT = os.path.join(DEPLOY_SCRIPT_DIR, 'measure_origination_size.js') + +DEFAULT_MAX_TOTAL_BYTES = 32768 + +# Taquito prepends a reveal to the same operation group when the deployer account has not +# yet revealed its public key, and that reveal shares the 32768-byte budget. A contract +# that only fits without the reveal cannot be originated as the deployer's first +# operation, so a margin thinner than this is reported as a warning. Value from Taquito's +# own REVEAL_LENGTH (324 hex chars, i.e. 162 bytes). +REVEAL_BYTES = 162 + +# Compile target file -> (compiled contract directory name, extra SmartPy CLI flags). +# The extra flags MUST match exactly what deploy_previewnet.sh/deploy_mainnet.sh pass +# for the same target (--erase-comments --erase-var-annots --initial-cast for +# Comptroller), otherwise this test measures a different (larger/smaller) artifact +# than what's actually deployed, which can produce a false pass or false fail against +# the safety threshold. See deploy/shell_scripts/deploy_*.sh for the source of truth. +COMPILE_TARGETS = { + 'CompileGovernance.py': ('Governance', []), + 'CompileTezFinOracle.py': ('TezFinOracle', []), + 'CompileComptroller.py': ('Comptroller', ['--erase-comments', '--erase-var-annots', '--initial-cast']), +} + + +def find_smartpy_cli(): + if len(sys.argv) > 1: + return sys.argv[1] + envPath = os.environ.get('SMARTPY_CLI') + if envPath: + return envPath + return os.path.expanduser('~/smartpy-cli/SmartPy.sh') + + +def read_sizes_csv(csvPath): + sizes = {} + with open(csvPath, newline='') as f: + for row in csv.reader(f): + if len(row) != 2: + continue + key, value = row + try: + sizes[key.strip()] = int(value.strip()) + except ValueError: + continue + return sizes + + +def find_sizes_csv(contractDir): + for name in sorted(os.listdir(contractDir)): + if name.endswith('_sizes.csv'): + return os.path.join(contractDir, name) + return None + + +def measure_origination_operation(contractDir): + """Returns the forged origination operation size for a compiled contract directory, + as {'forgedBytes': int, 'signatureBytes': int, 'opSize': int}. Raises RuntimeError if + the measurement could not be taken, so a broken forger fails the check instead of + silently falling back to the narrower code+storage number.""" + result = subprocess.run( + ['node', MEASURE_SCRIPT, contractDir, '--json', '--max', str(DEFAULT_MAX_TOTAL_BYTES)], + cwd=DEPLOY_SCRIPT_DIR, + capture_output=True, + text=True, + ) + # The script exits non-zero when the contract is over the limit, and that is a + # measurement we still want to report, so only treat unparseable output as an error. + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + raise RuntimeError( + f'could not measure the origination operation via {MEASURE_SCRIPT} ' + f'(exit {result.returncode}). Ensure `npm ci` has run in deploy/deploy_script so ' + f'@taquito/local-forging is available.\nstdout:\n{result.stdout.strip()[-1000:]}\n' + f'stderr:\n{result.stderr.strip()[-1000:]}' + ) + + +def main(): + maxTotalBytes = int(os.environ.get('MAX_CONTRACT_OPERATION_BYTES', DEFAULT_MAX_TOTAL_BYTES)) + smartpy = find_smartpy_cli() + + if not os.path.exists(smartpy): + print( + f'[ERROR] SmartPy CLI not found at "{smartpy}". Install it (see README "Run Contract ' + f'Unit Tests") or pass its path as an argument / SMARTPY_CLI env var. Refusing to skip ' + f'this check silently.' + ) + sys.exit(1) + + if not os.path.exists(PLACEHOLDER_MANIFEST): + print(f'[ERROR] Placeholder manifest not found at {PLACEHOLDER_MANIFEST}.') + sys.exit(1) + + tmpDir = tempfile.mkdtemp(prefix='tezfin_size_check_') + try: + failures = [] + checkedAny = False + for fileName, (contractName, extraFlags) in COMPILE_TARGETS.items(): + targetPath = os.path.join(REPO_ROOT, 'deploy', 'compile_targets', fileName) + result = subprocess.run( + [smartpy, 'compile', targetPath, tmpDir, '--purge', '--protocol', 'kathmandu', *extraFlags], + cwd=REPO_ROOT, + env={**os.environ, 'DEPLOY_MANIFEST': PLACEHOLDER_MANIFEST}, + capture_output=True, + text=True, + ) + if result.returncode != 0: + failures.append( + f'{contractName}: SmartPy compile of {fileName} failed (exit {result.returncode}). ' + f'stderr:\n{result.stderr.strip()[-2000:]}' + ) + continue + + contractDir = os.path.join(tmpDir, contractName) + if not os.path.isdir(contractDir): + failures.append(f'{contractName}: compile succeeded but expected output dir {contractDir} is missing') + continue + + csvPath = find_sizes_csv(contractDir) + if not csvPath: + failures.append(f'{contractName}: no *_sizes.csv found under {contractDir}') + continue + + sizes = read_sizes_csv(csvPath) + contractBytes = sizes.get('contract') + storageBytes = sizes.get('storage') + if contractBytes is None or storageBytes is None: + failures.append(f'{contractName}: {csvPath} is missing "contract" and/or "storage" rows') + continue + + try: + measured = measure_origination_operation(contractDir) + except RuntimeError as error: + failures.append(f'{contractName}: {error}') + continue + + checkedAny = True + codeAndStorage = contractBytes + storageBytes + opSize = measured['opSize'] + margin = maxTotalBytes - opSize + status = 'OK' if opSize <= maxTotalBytes else 'TOO LARGE' + print( + f'[INFO] {contractName}: code={contractBytes}B, storage={storageBytes}B ' + f'(code+storage={codeAndStorage}B); forged origination operation=' + f'{measured["forgedBytes"]}B + signature={measured["signatureBytes"]}B = ' + f'{opSize}B, margin={margin}B ({status})' + ) + if opSize > maxTotalBytes: + failures.append( + f'{contractName}: the origination operation is {opSize}B, which exceeds the ' + f'{maxTotalBytes}B protocol limit by {-margin}B, so it cannot be injected at all. ' + f'Note this is the size of the whole operation, not just code+storage ' + f'({codeAndStorage}B). Make the contract smaller; raising ' + f'MAX_CONTRACT_OPERATION_BYTES cannot make an over-limit operation injectable.' + ) + elif margin < REVEAL_BYTES: + print( + f'[WARN] {contractName}: only {margin}B of margin remains, which is less than the ' + f'~{REVEAL_BYTES}B a reveal operation adds. This contract can only be originated ' + f'from an account whose public key is already revealed, because Taquito batches the ' + f'reveal into the same operation group and the 32768B limit applies to the group. ' + f'Reduce the contract size before deploying from a fresh key.' + ) + + if not checkedAny: + print( + '[ERROR] No contracts were actually checked (every compile attempt failed or produced no ' + 'output). Treating this as a failure rather than silently passing.' + ) + failures.append('No contracts were successfully compiled/measured.') + + if failures: + print('Operation size check FAILED:') + for failure in failures: + print(f' - {failure}') + sys.exit(1) + + print( + f'Operation size check passed (forged origination operation must stay within ' + f'{maxTotalBytes}B per contract).' + ) + finally: + shutil.rmtree(tmpDir, ignore_errors=True) + + +if __name__ == '__main__': + main() diff --git a/deploy/compile_targets/tests/test_reproducible_build.py b/deploy/compile_targets/tests/test_reproducible_build.py new file mode 100644 index 00000000..eb6b609f --- /dev/null +++ b/deploy/compile_targets/tests/test_reproducible_build.py @@ -0,0 +1,138 @@ +"""Compile production-critical contracts twice and compare canonical artifacts.""" + +import hashlib +import json +import os +import subprocess +import sys +import tempfile + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +BASE_PLACEHOLDER_MANIFEST = os.path.join(REPO_ROOT, 'e2e', 'deploy_result', 'deploy.json') +COMPILE_TARGETS = { + 'CompileGovernance.py': (['Governance'], []), + 'CompileTezFinOracle.py': (['TezFinOracle'], []), + 'CompileComptroller.py': ( + ['Comptroller'], + ['--erase-comments', '--erase-var-annots', '--initial-cast'], + ), + 'CompileIRMs.py': (['CFA12_IRM', 'CFA2_IRM', 'CXTZ_IRM'], []), + 'CompileCtzBTC_IRM.py': (['CtzBTC_IRM'], []), + 'CompileCUSDt.py': ( + ['CUSDt'], + ['--erase-comments', '--erase-var-annots', '--initial-cast'], + ), + 'CompileCXTZ.py': ( + ['CXTZ'], + ['--erase-comments', '--erase-var-annots', '--initial-cast'], + ), + 'CompileTzBTC.py': ( + ['CtzBTC'], + ['--erase-comments', '--erase-var-annots', '--initial-cast'], + ), +} +PLACEHOLDER_ADDRESS = 'KT1ENe4jbDE1QVG1euryp23GsAeWuEwJutQX' + + +def find_smartpy_cli(): + if len(sys.argv) > 1: + return os.path.abspath(os.path.expanduser(sys.argv[1])) + return os.path.abspath(os.path.expanduser(os.environ.get('SMARTPY_CLI', '~/smartpy-cli/SmartPy.sh'))) + + +def find_single_file(directory, suffix): + matches = sorted(name for name in os.listdir(directory) if name.endswith(suffix)) + if len(matches) != 1: + raise RuntimeError(f'Expected one *{suffix} under {directory}, found {len(matches)}') + return os.path.join(directory, matches[0]) + + +def canonical_json_hash(file_path): + with open(file_path, encoding='utf-8') as source: + value = json.load(source) + encoded = json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=True).encode() + return hashlib.sha256(encoded).hexdigest() + + +def compile_once(smartpy, output_dir, manifest_path): + hashes = {} + for file_name, (contract_names, extra_flags) in COMPILE_TARGETS.items(): + target_path = os.path.join(REPO_ROOT, 'deploy', 'compile_targets', file_name) + result = subprocess.run( + [smartpy, 'compile', target_path, output_dir, '--purge', + '--protocol', 'kathmandu', *extra_flags], + cwd=REPO_ROOT, + env={**os.environ, 'DEPLOY_MANIFEST': manifest_path}, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError( + f'{file_name} compilation failed with exit {result.returncode}:\n' + f'{result.stderr.strip()[-4000:]}' + ) + + for contract_name in contract_names: + contract_dir = os.path.join(output_dir, contract_name) + hashes[contract_name] = { + 'contractSha256': canonical_json_hash( + find_single_file(contract_dir, '_contract.json') + ), + 'storageSha256': canonical_json_hash( + find_single_file(contract_dir, '_storage.json') + ), + } + return hashes + + +def write_placeholder_manifest(path): + with open(BASE_PLACEHOLDER_MANIFEST, encoding='utf-8') as source: + manifest = json.load(source) + for key in ('USDt', 'tzBTC', 'CtzBTC_IRM'): + manifest.setdefault(key, PLACEHOLDER_ADDRESS) + with open(path, 'w', encoding='utf-8') as destination: + json.dump(manifest, destination, sort_keys=True) + + +def main(): + smartpy = find_smartpy_cli() + if not os.path.isfile(smartpy): + raise RuntimeError(f'SmartPy CLI not found at {smartpy}') + if not os.path.isfile(BASE_PLACEHOLDER_MANIFEST): + raise RuntimeError(f'Placeholder manifest not found at {BASE_PLACEHOLDER_MANIFEST}') + + with tempfile.TemporaryDirectory(prefix='tezfin_repro_a_') as first_dir, \ + tempfile.TemporaryDirectory(prefix='tezfin_repro_b_') as second_dir, \ + tempfile.NamedTemporaryFile(mode='w', suffix='.json') as manifest_file: + write_placeholder_manifest(manifest_file.name) + first_hashes = compile_once(smartpy, first_dir, manifest_file.name) + second_hashes = compile_once(smartpy, second_dir, manifest_file.name) + + if first_hashes != second_hashes: + print('[ERROR] SmartPy builds are not reproducible:', file=sys.stderr) + print(json.dumps({'first': first_hashes, 'second': second_hashes}, indent=2), + file=sys.stderr) + return 1 + + output = { + 'smartpyVersion': '0.16.0', + 'protocol': 'kathmandu', + 'contracts': first_hashes, + } + output_path = os.environ.get('COMPILED_HASHES_OUTPUT') + if output_path: + with open(output_path, 'w', encoding='utf-8') as destination: + json.dump(output, destination, indent=2, sort_keys=True) + destination.write('\n') + + print(json.dumps(output, indent=2, sort_keys=True)) + print('[INFO] Two clean SmartPy builds produced identical canonical hashes.') + return 0 + + +if __name__ == '__main__': + try: + sys.exit(main()) + except (OSError, RuntimeError, subprocess.SubprocessError) as error: + print(f'[ERROR] {error}', file=sys.stderr) + sys.exit(1) diff --git a/deploy/deploy_script/.npmrc b/deploy/deploy_script/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/deploy/deploy_script/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/deploy/deploy_script/assert_network.js b/deploy/deploy_script/assert_network.js new file mode 100644 index 00000000..87cd56c1 --- /dev/null +++ b/deploy/deploy_script/assert_network.js @@ -0,0 +1,52 @@ +const { config, createTezosClient } = require('./util.js'); + +// Known mainnet chain ids. Extend if/when the protocol targets additional networks. +const MAINNET_CHAIN_IDS = new Set(['NetXdQprcVkpaWU']); + +// Pure decision logic (no network access), extracted so it can be unit-tested +// directly with fabricated chain ids instead of only against a live RPC connection. +// Returns nothing on success; throws with a descriptive message on rejection. +function checkNetworkExpectation(expectedProfile, declaredProfile, chainId, tezosNode) { + if (expectedProfile !== 'previewnet' && expectedProfile !== 'mainnet') { + throw new Error(`Unknown network profile "${expectedProfile}"; expected "previewnet" or "mainnet".`); + } + + if (declaredProfile && declaredProfile !== expectedProfile) { + throw new Error( + `This script is for "${expectedProfile}", but deploy_script/config.json declares ` + + `networkProfile "${declaredProfile}". Refusing to continue; point config.json at the ` + + `correct profile before running this script.`, + ); + } + + const isMainnetChain = MAINNET_CHAIN_IDS.has(chainId); + if (expectedProfile === 'mainnet' && !isMainnetChain) { + throw new Error( + `Expected a mainnet chain id, but the connected RPC (${tezosNode}) reports chain ` + + `${chainId}, which is not a known mainnet chain id. Refusing to run the mainnet deploy ` + + `script against what looks like a non-mainnet network.`, + ); + } + if (expectedProfile === 'previewnet' && isMainnetChain) { + throw new Error( + `The connected RPC (${tezosNode}) reports chain ${chainId}, which is a known ` + + `mainnet chain id. Refusing to run the Previewnet deploy script against mainnet.`, + ); + } +} + +async function assertNetwork(expectedProfile) { + const { chainId } = await createTezosClient(); + checkNetworkExpectation(expectedProfile, config.networkProfile, chainId, config.tezosNode); + console.log(`[INFO] Network check passed: profile=${expectedProfile}, chainId=${chainId}, node=${config.tezosNode}`); +} + +if (require.main === module) { + const expectedProfile = process.argv[2]; + assertNetwork(expectedProfile).catch((error) => { + console.error(`[ERROR] Network guard failed: ${error.message}`); + process.exitCode = 1; + }); +} + +module.exports = { assertNetwork, checkNetworkExpectation, MAINNET_CHAIN_IDS }; diff --git a/deploy/deploy_script/config.json b/deploy/deploy_script/config.json index baeb0c50..ff02189a 100644 --- a/deploy/deploy_script/config.json +++ b/deploy/deploy_script/config.json @@ -1,32 +1,9 @@ { - "tezosNode": "https://ghostnet.ecadinfra.com", - "conseilServer": { - "url": "https://conseil-ithaca.cryptonomic-infra.tech:443", - "apiKey": "328ac16b-5497-4e73-a97d-45af0e83425c", - "network": "ghostnet" - }, + "networkProfile": "mainnet", + "tezosNode": "https://rpc.tzkt.io/mainnet", + "chainId": "NetXdQprcVkpaWU", + "feeSafetyMultiplier": 1.2, "originator": { - "pkh": "tz1VLnrVYrMtLHRUfLV594uvzSthZ5w7wXqE", - "mnemonic": [ - "egg", - "zoo", - "cigar", - "oppose", - "shrimp", - "chair", - "wood", - "practice", - "trash", - "rice", - "spell", - "unhappy", - "disorder", - "zebra", - "kite" - ], - "email": "zdnmiequ.ulruuquy@teztnets.xyz", - "password": "dlfsJnRlHy", - "amount": "22841330521", - "activation_code": "77c3e768abfdb7e389f7ccd890c091ba42b84a88" + "pkh": "tz1XTWbfhyWK9xmPAa6TyQUSv437JFgZDzgA" } -} \ No newline at end of file +} diff --git a/deploy/deploy_script/deploy.js b/deploy/deploy_script/deploy.js index c6f4ee43..6d9ac317 100644 --- a/deploy/deploy_script/deploy.js +++ b/deploy/deploy_script/deploy.js @@ -1,2 +1,5 @@ const { run } = require("./util.js"); -run(); +run().catch((error) => { + console.error(`[ERROR] Deployment failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/deploy/deploy_script/deploy_e2e.js b/deploy/deploy_script/deploy_e2e.js index 7ac73401..cb5dc0ac 100644 --- a/deploy/deploy_script/deploy_e2e.js +++ b/deploy/deploy_script/deploy_e2e.js @@ -1,2 +1,5 @@ const {runE2E} = require("./util.js") -runE2E(); +runE2E().catch((error) => { + console.error(`[ERROR] E2E deployment failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/deploy/deploy_script/estimate_origination.js b/deploy/deploy_script/estimate_origination.js new file mode 100644 index 00000000..fa0bbde3 --- /dev/null +++ b/deploy/deploy_script/estimate_origination.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); + +const { TezosToolkit } = require('@taquito/taquito'); + +const config = require('./config.json'); + +class PublicKeySigner { + constructor(publicKeyHash, publicKey) { + this.publicKeyHashValue = publicKeyHash; + this.publicKeyValue = publicKey; + } + + async publicKeyHash() { + return this.publicKeyHashValue; + } + + async publicKey() { + return this.publicKeyValue; + } + + async secretKey() { + throw new Error('Read-only estimator has no secret key.'); + } + + async sign() { + throw new Error('Read-only estimator cannot sign or inject operations.'); + } +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +async function main() { + const [contractPath, storagePath] = process.argv.slice(2); + if (!contractPath || !storagePath) { + throw new Error('Usage: node estimate_origination.js CONTRACT.json STORAGE.json'); + } + + const rpc = process.env.TEZOS_RPC || config.tezosNode; + const publicKeyHash = process.env.TEZOS_PUBLIC_KEY_HASH || config.originator?.pkh; + const publicKey = process.env.TEZOS_PUBLIC_KEY; + if (!rpc || !publicKeyHash || !publicKey) { + throw new Error('TEZOS_RPC, TEZOS_PUBLIC_KEY_HASH, and TEZOS_PUBLIC_KEY are required.'); + } + + const tezos = new TezosToolkit(rpc); + tezos.setProvider({ signer: new PublicKeySigner(publicKeyHash, publicKey) }); + + const chainId = await tezos.rpc.getChainId(); + if (config.chainId && chainId !== config.chainId && !process.env.TEZOS_RPC) { + throw new Error(`Configured chain id ${config.chainId} does not match RPC chain id ${chainId}.`); + } + + const estimate = await tezos.estimate.originate({ + balance: '0', + code: readJson(path.resolve(contractPath)), + init: readJson(path.resolve(storagePath)), + }); + console.log(JSON.stringify({ + rpc, + chainId, + source: publicKeyHash, + suggestedFeeMutez: estimate.suggestedFeeMutez, + gasLimit: estimate.gasLimit, + storageLimit: estimate.storageLimit, + operationSize: estimate.opSize, + }, null, 2)); +} + +main().catch((error) => { + console.error(`[ERROR] Origination estimate failed: ${error.message}`); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/deploy/deploy_script/mainnet_preflight.js b/deploy/deploy_script/mainnet_preflight.js new file mode 100644 index 00000000..ebf62ab9 --- /dev/null +++ b/deploy/deploy_script/mainnet_preflight.js @@ -0,0 +1,138 @@ +const { config, createTezosClient, resolveDeployResultPath } = require('./util.js'); + +const fs = require('fs'); + +function readDeployResult(jsonPath) { + if (!fs.existsSync(jsonPath)) { + return {}; + } + return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); +} + +// Addresses that a mainnet deployment must never originate itself: they are external, +// canonical assets/services this protocol integrates with, not contracts this repo +// controls. They must be supplied explicitly in the mainnet manifest ahead of time. +// +// Only list keys that a currently-wired compile target actually consumes. USDtz is +// intentionally NOT required here: no compile target in deploy_mainnet.sh/ +// deploy_previewnet.sh originates a CUSDtz market (CompileCUSDtz.py is not part of +// either pipeline), so requiring it here would block deployments that never need it. +// Add it back only once a CUSDtz market is actually added to the mainnet pipeline. +const REQUIRED_CANONICAL_KEYS = ['PriceOracle', 'USDt', 'tzBTC']; + +// Vetted production addresses for canonical external assets/services. These must be +// filled in with the exact, reviewed mainnet addresses before a real mainnet +// deployment; a manifest value that doesn't match exactly is rejected rather than +// silently accepted just because *some* contract exists at that address. Deliberately +// left null/empty until vetted mainnet addresses are confirmed for this deployment; +// mainnet preflight fails closed while any required entry is absent. +const VETTED_MAINNET_ADDRESSES = { + // Blocked after the 2026-07-15 Kolibri incident. KT1B74... proxies the + // affected KT1Exb... feed; add an address only after independent review. + PriceOracle: null, + USDt: 'KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o', + tzBTC: 'KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn', +}; + +async function verifyAddressExists(tezos, key, address) { + try { + await tezos.rpc.getScript(address); + } catch (error) { + throw new Error( + `Manifest key "${key}" (${address}) could not be found on the connected chain (${error.message}).`, + ); + } +} + +function verifyAgainstAllowlist(key, address) { + const expected = VETTED_MAINNET_ADDRESSES[key]; + if (!expected) { + throw new Error( + `No vetted mainnet address is configured for required key "${key}" in ` + + `VETTED_MAINNET_ADDRESSES. Refusing to proceed until the exact reviewed production ` + + `address is added to mainnet_preflight.js.`, + ); + } + if (address !== expected) { + throw new Error( + `Manifest key "${key}" is ${address}, but the vetted mainnet address is ${expected}. ` + + `Refusing to proceed with a mismatched canonical address.`, + ); + } +} + +// Pure check (no network access) split out from mainnetPreflight so the "missing +// required canonical key" rejection path can be unit-tested directly. +function findMissingCanonicalKeys(deployResult) { + return REQUIRED_CANONICAL_KEYS.filter((key) => !deployResult[key]); +} + +async function mainnetPreflight(deployResultPath) { + const { tezos, chainId } = await createTezosClient(); + const deployResult = readDeployResult(deployResultPath); + + const missing = findMissingCanonicalKeys(deployResult); + if (missing.length > 0) { + throw new Error( + `Mainnet manifest ${deployResultPath} is missing required canonical addresses: ` + + `[${missing.join(', ')}]. A mainnet deployment must never originate test tokens or a mock ` + + `oracle (see CompileTestData.py, which this script refuses to run); populate these keys ` + + `with the vetted production addresses before continuing. See README.md "PriceOracle ` + + `Configuration".`, + ); + } + + console.log('[INFO] Verifying required canonical addresses exist on-chain and match the allowlist...'); + for (const key of REQUIRED_CANONICAL_KEYS) { + verifyAgainstAllowlist(key, deployResult[key]); + await verifyAddressExists(tezos, key, deployResult[key]); + console.log(`[INFO] ${key}: ${deployResult[key]} OK`); + } + + console.log(''); + console.log('==================== MAINNET DEPLOYMENT PLAN ===================='); + console.log(`Network: ${config.tezosNode}`); + console.log(`Chain ID: ${chainId}`); + console.log(`Manifest: ${deployResultPath}`); + console.log('Canonical inputs (not originated by this deployment):'); + for (const key of REQUIRED_CANONICAL_KEYS) { + console.log(` - ${key}: ${deployResult[key]}`); + } + console.log('Contracts already recorded in the manifest (will be verified, not re-originated):'); + const alreadyDeployedKeys = Object.keys(deployResult).filter( + (key) => !REQUIRED_CANONICAL_KEYS.includes(key) && !['chainId', 'network', 'OriginatorAddress'].includes(key), + ); + if (alreadyDeployedKeys.length === 0) { + console.log(' (none — this will be a full fresh deployment)'); + } else { + for (const key of alreadyDeployedKeys) { + console.log(` - ${key}: ${deployResult[key]}`); + } + } + console.log('==================================================================='); + console.log(''); + + if (process.env.MAINNET_DEPLOY_CONFIRM !== 'yes') { + throw new Error( + 'Refusing to proceed without explicit confirmation. Review the plan above carefully, ' + + 'then re-run with MAINNET_DEPLOY_CONFIRM=yes to continue.', + ); + } + + console.log('[INFO] MAINNET_DEPLOY_CONFIRM=yes acknowledged; proceeding.'); +} + +if (require.main === module) { + mainnetPreflight(resolveDeployResultPath()).catch((error) => { + console.error(`[ERROR] Mainnet preflight failed: ${error.message}`); + process.exitCode = 1; + }); +} + +module.exports = { + mainnetPreflight, + findMissingCanonicalKeys, + verifyAgainstAllowlist, + REQUIRED_CANONICAL_KEYS, + VETTED_MAINNET_ADDRESSES, +}; diff --git a/deploy/deploy_script/measure_origination_size.js b/deploy/deploy_script/measure_origination_size.js new file mode 100644 index 00000000..d04cbec0 --- /dev/null +++ b/deploy/deploy_script/measure_origination_size.js @@ -0,0 +1,160 @@ +// Measures the size of the *complete* origination operation for a compiled contract, +// offline. This exists because SmartPy's `*_sizes.csv` only reports the packed +// Micheline size of the contract code and the initial storage value. The protocol's +// `max_operation_data_length` (32768) applies to the whole signed manager operation, so +// code+storage systematically under-reports the number that actually has to fit. +// +// The value printed here is the same quantity Taquito reports as `estimate.opSize`: +// the forged operation bytes plus the signature that gets appended at injection +// (see @taquito/taquito rpc-estimate-provider.js, which computes +// `opbytes.length / 2 + payloadLength[Ed25519Signature]`). Forging locally with +// @taquito/local-forging means this needs no RPC, no funded account, and no secret key, +// so it can run in CI on every commit instead of only at deploy time. +// +// Envelope fields (fee/counter/gas_limit/storage_limit) are zarith-encoded, so their +// byte length depends on their magnitude. We deliberately forge with values at or above +// anything a real origination would use, which makes the reported size an upper bound: +// if this passes, the real origination is no larger. +const fs = require('fs'); +const path = require('path'); + +const { LocalForger } = require('@taquito/local-forging'); +const { PrefixV2, b58Encode, payloadLength } = require('@taquito/utils'); + +const DEFAULT_MAX_TOTAL_BYTES = 32768; + +// Ed25519 (tz1) signature payload length, read from Taquito rather than hardcoded so this +// stays in step with the same table its estimator uses. +const SIGNATURE_BYTES = payloadLength[PrefixV2.Ed25519Signature]; + +// Deliberately pessimistic envelope values. Each is >= what a real origination needs, +// so the zarith encodings are at least as long as the ones that get injected: +// fee - far above any observed origination fee +// counter - 9 digits, beyond current mainnet counters +// gas_limit - the per-operation hard gas cap +// storage_limit - well above a 32KB contract's storage cost +// balance is '0' to match deployMichelsonContract() in util.js, which originates with +// no initial balance. +const WORST_CASE_ENVELOPE = { + fee: '1000000', + counter: '999999999', + gas_limit: '1040000', + storage_limit: '999999', + balance: '0', +}; + +// Any well-formed values work for forging; only their encoded length matters, and both of +// these are fixed-width. The branch is a block hash (32 bytes) and the source is an +// implicit account (21 bytes), regardless of which block or account is used. +const PLACEHOLDER_BRANCH = b58Encode(new Uint8Array(32).fill(0x11), PrefixV2.BlockHash); +const PLACEHOLDER_SOURCE = 'tz1XTWbfhyWK9xmPAa6TyQUSv437JFgZDzgA'; + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function resolveArtifactPaths(contractDir) { + const entries = fs.readdirSync(contractDir); + const contractFile = entries.find((name) => /_contract\.json$/.test(name)); + const storageFile = entries.find((name) => /_storage\.json$/.test(name)); + if (!contractFile || !storageFile) { + throw new Error( + `${contractDir} does not look like a SmartPy output directory: expected a ` + + `*_contract.json and a *_storage.json (found: ${entries.join(', ') || 'nothing'}).`, + ); + } + return { + contractPath: path.join(contractDir, contractFile), + storagePath: path.join(contractDir, storageFile), + }; +} + +async function measureOriginationSize({ contractPath, storagePath }) { + const forger = new LocalForger(); + const opBytes = await forger.forge({ + branch: PLACEHOLDER_BRANCH, + contents: [{ + kind: 'origination', + source: PLACEHOLDER_SOURCE, + ...WORST_CASE_ENVELOPE, + script: { + code: readJson(contractPath), + storage: readJson(storagePath), + }, + }], + }); + + const forgedBytes = opBytes.length / 2; + return { + forgedBytes, + signatureBytes: SIGNATURE_BYTES, + opSize: forgedBytes + SIGNATURE_BYTES, + }; +} + +function parseArgs(argv) { + const args = { contractDir: null, maxTotalBytes: DEFAULT_MAX_TOTAL_BYTES, json: false }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--json') { + args.json = true; + } else if (arg === '--max') { + args.maxTotalBytes = Number(argv[i + 1]); + i += 1; + } else if (!args.contractDir) { + args.contractDir = arg; + } else { + throw new Error(`Unexpected argument "${arg}".`); + } + } + if (!args.contractDir) { + throw new Error( + 'Usage: node measure_origination_size.js [--max BYTES] [--json]\n' + + 'e.g. node measure_origination_size.js ../../TezFinBuild/compiled_contracts/Comptroller', + ); + } + if (!Number.isFinite(args.maxTotalBytes) || args.maxTotalBytes <= 0) { + throw new Error('--max must be a positive number of bytes.'); + } + return args; +} + +async function main() { + const { contractDir, maxTotalBytes, json } = parseArgs(process.argv.slice(2)); + const measurement = await measureOriginationSize(resolveArtifactPaths(contractDir)); + const margin = maxTotalBytes - measurement.opSize; + const withinLimit = measurement.opSize <= maxTotalBytes; + + if (json) { + console.log(JSON.stringify({ + contractDir, ...measurement, maxTotalBytes, margin, withinLimit, + }, null, 2)); + } else { + console.log(`[INFO] ${path.basename(contractDir)}: forged=${measurement.forgedBytes}B + ` + + `signature=${measurement.signatureBytes}B = opSize ${measurement.opSize}B ` + + `(limit ${maxTotalBytes}B, margin ${margin}B)`); + } + + if (!withinLimit) { + console.error( + `[FAIL] ${path.basename(contractDir)} origination operation is ${measurement.opSize}B, ` + + `which exceeds the ${maxTotalBytes}B limit by ${-margin}B. This operation cannot be ` + + 'injected; the contract must be made smaller.', + ); + process.exitCode = 1; + } +} + +if (require.main === module) { + main().catch((error) => { + console.error(error.message); + process.exit(1); + }); +} + +module.exports = { + measureOriginationSize, + resolveArtifactPaths, + DEFAULT_MAX_TOTAL_BYTES, + SIGNATURE_BYTES, +}; diff --git a/deploy/deploy_script/package-lock.json b/deploy/deploy_script/package-lock.json index 50b63cd8..ba1b8ea9 100644 --- a/deploy/deploy_script/package-lock.json +++ b/deploy/deploy_script/package-lock.json @@ -8,500 +8,531 @@ "name": "TezFinDeploy", "version": "1.0.0", "dependencies": { - "conseiljs": "5.3.0", - "conseiljs-softsigner": "^5.0.4-1", - "glob": "7.2.3", - "loglevel": "^1.8.1", - "node-fetch": "^2.6.9" + "@taquito/local-forging": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/taquito": "^25.0.0", + "@taquito/utils": "^25.0.0", + "glob": "^13.0.6", + "loglevel": "^1.8.1" }, - "devDependencies": {} - }, - "node_modules/@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "dependencies": { - "safe-buffer": "^5.0.1" + "devDependencies": {}, + "engines": { + "node": "22.16.0" } }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, "engines": { - "node": ">=0.6" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/bignumber.js": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", - "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", "engines": { - "node": "*" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/bip39": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", - "integrity": "sha512-P0dKrz4g0V0BjXfx7d9QNkJ/Txcz/k+hM9TnjqjUaXtuOfAvxXSw2rJw8DX0e3ZPwnK/IgDxoRqf0bvoVCqbMg==", - "dependencies": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/blakejs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", - "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" - }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "node_modules/@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", + "license": "MIT", "dependencies": { - "base-x": "^3.0.2" + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "node_modules/@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "license": "MIT", "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" + "@stablelib/int": "^1.0.1" } }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } + "node_modules/@stablelib/bytes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz", + "integrity": "sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==", + "license": "MIT" }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "node_modules/@stablelib/constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", + "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==", + "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==", + "license": "MIT" }, - "node_modules/conseiljs": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/conseiljs/-/conseiljs-5.3.0.tgz", - "integrity": "sha512-TaP3WbMSORcX/pNHM2L0PV3vwd7aX8bdwo49frtXCPUwlJEQj5/uAfeNAIdmvZHD90cYQg/s/9nR0+YP3ADbVQ==", + "node_modules/@stablelib/keyagreement": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz", + "integrity": "sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==", + "license": "MIT", "dependencies": { - "big-integer": "1.6.51", - "bignumber.js": "9.0.2", - "blakejs": "1.1.0", - "bs58check": "2.1.2", - "jsonpath-plus": "6.0.1", - "moo": "0.5.0", - "nearley": "2.19.1" - }, - "engines": { - "node": ">=14.18.2", - "npm": ">=6.14.15" + "@stablelib/bytes": "^1.0.1" } }, - "node_modules/conseiljs-softsigner": { - "version": "5.0.4-1", - "resolved": "https://registry.npmjs.org/conseiljs-softsigner/-/conseiljs-softsigner-5.0.4-1.tgz", - "integrity": "sha512-dZ6PyEcoBn6gJNO3B6+3LiW7fXBcGdAbAfjLU5hYgtLImP8W7jtADpqY80weWeJz7PRpSn9bmNK5atrCT+cBSw==", + "node_modules/@stablelib/nacl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@stablelib/nacl/-/nacl-1.0.4.tgz", + "integrity": "sha512-PJ2U/MrkXSKUM8C4qFs87WeCNxri7KQwR8Cdwm9q2sweGuAtTvOJGuW0F3N+zn+ySLPJA98SYWSSpogMJ1gCmw==", + "license": "MIT", "dependencies": { - "bip39": "3.0.3", - "conseiljs": "5.3.0", - "ed25519-hd-key": "1.1.2", - "libsodium-wrappers-sumo": "0.7.8", - "secp256k1": "4.0.2" - }, - "engines": { - "node": ">=12.20.1", - "npm": ">=6.14.10" + "@stablelib/poly1305": "^1.0.1", + "@stablelib/random": "^1.0.2", + "@stablelib/wipe": "^1.0.1", + "@stablelib/x25519": "^1.0.3", + "@stablelib/xsalsa20": "^1.0.2" } }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "node_modules/@stablelib/poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", + "integrity": "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==", + "license": "MIT", "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "node_modules/@stablelib/random": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", + "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", + "license": "MIT", "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=" - }, - "node_modules/ed25519-hd-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ed25519-hd-key/-/ed25519-hd-key-1.1.2.tgz", - "integrity": "sha512-/0y9y6N7vM6Kj5ASr9J9wcMVDTtygxSOvYX+PJiMD7VcxCx2G03V5bLRl8Dug9EgkLFsLhGqBtQWQRcElEeWTA==", + "node_modules/@stablelib/salsa20": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/salsa20/-/salsa20-1.0.2.tgz", + "integrity": "sha512-nfjKzw0KTKrrKBasEP+j7UP4I8Xudom8lVZIBCp0kQNARXq72IlSic0oabg2FC1NU68L4RdHrNJDd8bFwrphYA==", + "license": "MIT", "dependencies": { - "bip39": "3.0.2", - "create-hmac": "1.1.7", - "tweetnacl": "1.0.3" + "@stablelib/binary": "^1.0.1", + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "node_modules/ed25519-hd-key/node_modules/bip39": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", - "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "node_modules/@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", + "license": "MIT" + }, + "node_modules/@stablelib/x25519": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-1.0.3.tgz", + "integrity": "sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==", + "license": "MIT", "dependencies": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" + "@stablelib/keyagreement": "^1.0.1", + "@stablelib/random": "^1.0.2", + "@stablelib/wipe": "^1.0.1" } }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "node_modules/@stablelib/xsalsa20": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/xsalsa20/-/xsalsa20-1.0.2.tgz", + "integrity": "sha512-7XdBGbcNgBShmuhDXv1G1WPVCkjZdkb1oPMzSidO7Fve0MHntH6TjFkj5bfLI+aRE+61weO076vYpP/jmaAYog==", + "license": "MIT", "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" + "@stablelib/binary": "^1.0.1", + "@stablelib/salsa20": "^1.0.2", + "@stablelib/wipe": "^1.0.1" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/@taquito/core": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/core/-/core-25.0.0.tgz", + "integrity": "sha512-TlU+eMEn47mAmIjTFP2U4ReHRU05FsnyVn/c5DQnJC0psZ6LRJXimAIA1KQ94afsCDByewsXLN5fjKwn47l72A==", + "license": "Apache-2.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "json-stringify-safe": "^5.0.1" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=22" } }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "node_modules/@taquito/http-utils": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/http-utils/-/http-utils-25.0.0.tgz", + "integrity": "sha512-/nkwDeTXEW10lSQdyTrLgp9GIWQc2yi6wQLjEFvXveevrMjuA+lfFU8QoOOZxXH0GM42KEmU/y/VA1wcgl+gKQ==", + "license": "Apache-2.0", "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "@taquito/core": "^25.0.0" }, "engines": { - "node": ">=4" + "node": ">=22" } }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "node_modules/@taquito/local-forging": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/local-forging/-/local-forging-25.0.0.tgz", + "integrity": "sha512-sFeObpxya59PP5xFFsPWb3zLxASe3QwQqQ1fJWTPCs/Fph7/cxk5Kj0Oco961SC7CVSkG/ayz4/WINmm+oTlPA==", + "license": "Apache-2.0", "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "@taquito/core": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "fast-text-encoding": "^1.0.6" + }, + "engines": { + "node": ">=22" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } + "node_modules/@taquito/local-forging/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/@taquito/michel-codec": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/michel-codec/-/michel-codec-25.0.0.tgz", + "integrity": "sha512-ITM3lM3tEAblPVdzQk5Rv2nLMMOalrXbnIt5b5St8OVYwqaDyfsH3fxSGIVDPxRQyyBdgJ1403aVu6768/8m4Q==", + "license": "Apache-2.0", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/jsonpath-plus": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", - "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==", + "@taquito/core": "^25.0.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">=22" } }, - "node_modules/libsodium-sumo": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.9.tgz", - "integrity": "sha512-DcfJ57zlSlcmQU4s8KOX78pT0zKx5S9RLi0oyDuoIgm4K95+VNSaOidK/y9lUK4lxft14PtTPjoBy8tmLk1TDQ==" - }, - "node_modules/libsodium-wrappers-sumo": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.8.tgz", - "integrity": "sha512-a36GRLb59E5Zx8br6Xvwk6QsnYWodkCJFvNQxnhrvL9t9i4v6v3V/cv3h0/pxehf35ljVCWqoi0CawMQ8C1JmA==", + "node_modules/@taquito/michelson-encoder": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/michelson-encoder/-/michelson-encoder-25.0.0.tgz", + "integrity": "sha512-BWwAAwBhY3zngcMK8SkmBWDZ5aNVRnHlHvi7Xu6jnGW109yM3n9awxxCrFD8jOXPb3dly+/pulGnkv28+EvIIQ==", + "license": "Apache-2.0", "dependencies": { - "libsodium-sumo": "^0.7.0" - } - }, - "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", - "engines": { - "node": ">= 0.6.0" + "@taquito/core": "^25.0.0", + "@taquito/rpc": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "fast-json-stable-stringify": "^2.1.0" }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" + "engines": { + "node": ">=22" } }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "node_modules/@taquito/michelson-encoder/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" + }, + "node_modules/@taquito/rpc": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/rpc/-/rpc-25.0.0.tgz", + "integrity": "sha512-qqV4boldGOavsM6ZzK6xky4xAEyllYDyIO5C16cvkoUzoMCCsWMpRuXa1csSrQc+0IECkkDurNLTbrMJPne9tA==", + "license": "Apache-2.0", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "@taquito/core": "^25.0.0", + "@taquito/http-utils": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2" + }, + "engines": { + "node": ">=22" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + "node_modules/@taquito/rpc/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@taquito/signer": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/signer/-/signer-25.0.0.tgz", + "integrity": "sha512-UDHwWd3O0VwZjbEw0NDsoQoIhoQv+xHxb/XAKW7ruSjUT6OkH5whhHpgTZ+T2eJLPDBZLY1RH8IdSl+6ZFtUXg==", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.2.0", + "@scure/bip39": "^2.2.0", + "@stablelib/nacl": "^1.0.4", + "@taquito/core": "^25.0.0", + "@taquito/utils": "^25.0.0", + "@types/bn.js": "^5.1.5", + "bn.js": "^5.2.2", + "typedarray-to-buffer": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=22" } }, - "node_modules/moo": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.0.tgz", - "integrity": "sha512-AMv6iqhTEd5vT/cQlH6cammKS5ekyHhyqTRKi5zKMWl1RTyFnQ3ohPSBNSm8ySe2wlxSKwDonr9D5ZT44mdO3g==" + "node_modules/@taquito/signer/node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" }, - "node_modules/nearley": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.1.tgz", - "integrity": "sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==", + "node_modules/@taquito/taquito": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/taquito/-/taquito-25.0.0.tgz", + "integrity": "sha512-MBNKXK1CrOlwBrI0IkjWbVcuwY238ZF8B6fuf2qA0uYp+UmVjukUJ4QCvRf+bayWzfiWBbkYP0upY6VGV3C1VQ==", + "license": "Apache-2.0", "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" + "@taquito/core": "^25.0.0", + "@taquito/http-utils": "^25.0.0", + "@taquito/local-forging": "^25.0.0", + "@taquito/michel-codec": "^25.0.0", + "@taquito/michelson-encoder": "^25.0.0", + "@taquito/rpc": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "rxjs": "^7.8.2" }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" + "engines": { + "node": ">=22" } }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + "node_modules/@taquito/taquito/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "node_modules/@taquito/utils": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/utils/-/utils-25.0.0.tgz", + "integrity": "sha512-I/9hndZcx0yL6f1djPMhgxCpAopbh/E1ta5kYGs3AxDGrYKt0QpbWAvvUV7fwU9kgzHRQlm7fX8DP7zUbz7ntg==", + "license": "Apache-2.0", "dependencies": { - "whatwg-url": "^5.0.0" + "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.2.0", + "@taquito/core": "^25.0.0", + "@types/bs58check": "^2.1.2", + "bignumber.js": "^10.0.2", + "blakejs": "^1.2.1", + "bs58check": "^3.0.1", + "buffer": "^6.0.3", + "typedarray-to-buffer": "^4.0.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=22" } }, - "node_modules/node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "node_modules/@taquito/utils/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/@taquito/utils/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" + }, + "node_modules/@taquito/utils/node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/@taquito/utils/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/@taquito/utils/node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "license": "MIT", "dependencies": { - "wrappy": "1" + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "node_modules/@taquito/utils/node_modules/bs58check/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" + "@types/node": "*" } }, - "node_modules/railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=" - }, - "node_modules/randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "node_modules/@types/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-xpXaQlOIY1KoXlA/ytHGHpEIU87PJt+g9SH7nC6HdCgaBwT2IEZIwBMHbjuX6BpnfbiUMlmwqurdLDwXpcdmSA==", + "license": "MIT", "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" + "@types/node": "*" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" + "node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 6" + "node": "18 || 20 || >=22" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "engines": { - "node": ">=0.12" + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "license": "Apache-2.0" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -515,567 +546,554 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, - "node_modules/secp256k1": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", - "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", "engines": { - "node": ">=10.0.0" + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" } }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" }, - "bin": { - "sha.js": "bin.js" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tslib": "^2.1.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typedarray-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-4.0.0.tgz", + "integrity": "sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" } }, "dependencies": { - "@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "requires": { - "safe-buffer": "^5.0.1" + "@noble/hashes": "1.8.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + } } }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + "@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==" }, - "bignumber.js": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", - "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + "@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==" }, - "bip39": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", - "integrity": "sha512-P0dKrz4g0V0BjXfx7d9QNkJ/Txcz/k+hM9TnjqjUaXtuOfAvxXSw2rJw8DX0e3ZPwnK/IgDxoRqf0bvoVCqbMg==", + "@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", "requires": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" } }, - "blakejs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", - "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@stablelib/int": "^1.0.1" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + "@stablelib/bytes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz", + "integrity": "sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==" }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } + "@stablelib/constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", + "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==" }, - "bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" + }, + "@stablelib/keyagreement": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz", + "integrity": "sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==", "requires": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" + "@stablelib/bytes": "^1.0.1" } }, - "cipher-base": { + "@stablelib/nacl": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "resolved": "https://registry.npmjs.org/@stablelib/nacl/-/nacl-1.0.4.tgz", + "integrity": "sha512-PJ2U/MrkXSKUM8C4qFs87WeCNxri7KQwR8Cdwm9q2sweGuAtTvOJGuW0F3N+zn+ySLPJA98SYWSSpogMJ1gCmw==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "@stablelib/poly1305": "^1.0.1", + "@stablelib/random": "^1.0.2", + "@stablelib/wipe": "^1.0.1", + "@stablelib/x25519": "^1.0.3", + "@stablelib/xsalsa20": "^1.0.2" } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "conseiljs": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/conseiljs/-/conseiljs-5.3.0.tgz", - "integrity": "sha512-TaP3WbMSORcX/pNHM2L0PV3vwd7aX8bdwo49frtXCPUwlJEQj5/uAfeNAIdmvZHD90cYQg/s/9nR0+YP3ADbVQ==", - "requires": { - "big-integer": "1.6.51", - "bignumber.js": "9.0.2", - "blakejs": "1.1.0", - "bs58check": "2.1.2", - "jsonpath-plus": "6.0.1", - "moo": "0.5.0", - "nearley": "2.19.1" - } - }, - "conseiljs-softsigner": { - "version": "5.0.4-1", - "resolved": "https://registry.npmjs.org/conseiljs-softsigner/-/conseiljs-softsigner-5.0.4-1.tgz", - "integrity": "sha512-dZ6PyEcoBn6gJNO3B6+3LiW7fXBcGdAbAfjLU5hYgtLImP8W7jtADpqY80weWeJz7PRpSn9bmNK5atrCT+cBSw==", + "@stablelib/poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", + "integrity": "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==", "requires": { - "bip39": "3.0.3", - "conseiljs": "5.3.0", - "ed25519-hd-key": "1.1.2", - "libsodium-wrappers-sumo": "0.7.8", - "secp256k1": "4.0.2" + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "@stablelib/random": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", + "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "@stablelib/salsa20": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/salsa20/-/salsa20-1.0.2.tgz", + "integrity": "sha512-nfjKzw0KTKrrKBasEP+j7UP4I8Xudom8lVZIBCp0kQNARXq72IlSic0oabg2FC1NU68L4RdHrNJDd8bFwrphYA==", "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "@stablelib/binary": "^1.0.1", + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" } }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=" + "@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" }, - "ed25519-hd-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ed25519-hd-key/-/ed25519-hd-key-1.1.2.tgz", - "integrity": "sha512-/0y9y6N7vM6Kj5ASr9J9wcMVDTtygxSOvYX+PJiMD7VcxCx2G03V5bLRl8Dug9EgkLFsLhGqBtQWQRcElEeWTA==", + "@stablelib/x25519": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-1.0.3.tgz", + "integrity": "sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==", "requires": { - "bip39": "3.0.2", - "create-hmac": "1.1.7", - "tweetnacl": "1.0.3" - }, - "dependencies": { - "bip39": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", - "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", - "requires": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - } - } + "@stablelib/keyagreement": "^1.0.1", + "@stablelib/random": "^1.0.2", + "@stablelib/wipe": "^1.0.1" } }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "@stablelib/xsalsa20": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/xsalsa20/-/xsalsa20-1.0.2.tgz", + "integrity": "sha512-7XdBGbcNgBShmuhDXv1G1WPVCkjZdkb1oPMzSidO7Fve0MHntH6TjFkj5bfLI+aRE+61weO076vYpP/jmaAYog==", "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" + "@stablelib/binary": "^1.0.1", + "@stablelib/salsa20": "^1.0.2", + "@stablelib/wipe": "^1.0.1" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "@taquito/core": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/core/-/core-25.0.0.tgz", + "integrity": "sha512-TlU+eMEn47mAmIjTFP2U4ReHRU05FsnyVn/c5DQnJC0psZ6LRJXimAIA1KQ94afsCDByewsXLN5fjKwn47l72A==", "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "json-stringify-safe": "^5.0.1" } }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "@taquito/http-utils": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/http-utils/-/http-utils-25.0.0.tgz", + "integrity": "sha512-/nkwDeTXEW10lSQdyTrLgp9GIWQc2yi6wQLjEFvXveevrMjuA+lfFU8QoOOZxXH0GM42KEmU/y/VA1wcgl+gKQ==", "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "@taquito/core": "^25.0.0" } }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "@taquito/local-forging": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/local-forging/-/local-forging-25.0.0.tgz", + "integrity": "sha512-sFeObpxya59PP5xFFsPWb3zLxASe3QwQqQ1fJWTPCs/Fph7/cxk5Kj0Oco961SC7CVSkG/ayz4/WINmm+oTlPA==", "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "@taquito/core": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "fast-text-encoding": "^1.0.6" + }, + "dependencies": { + "bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==" + } } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "@taquito/michel-codec": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/michel-codec/-/michel-codec-25.0.0.tgz", + "integrity": "sha512-ITM3lM3tEAblPVdzQk5Rv2nLMMOalrXbnIt5b5St8OVYwqaDyfsH3fxSGIVDPxRQyyBdgJ1403aVu6768/8m4Q==", "requires": { - "once": "^1.3.0", - "wrappy": "1" + "@taquito/core": "^25.0.0" } }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "jsonpath-plus": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", - "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==" - }, - "libsodium-sumo": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.9.tgz", - "integrity": "sha512-DcfJ57zlSlcmQU4s8KOX78pT0zKx5S9RLi0oyDuoIgm4K95+VNSaOidK/y9lUK4lxft14PtTPjoBy8tmLk1TDQ==" - }, - "libsodium-wrappers-sumo": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.8.tgz", - "integrity": "sha512-a36GRLb59E5Zx8br6Xvwk6QsnYWodkCJFvNQxnhrvL9t9i4v6v3V/cv3h0/pxehf35ljVCWqoi0CawMQ8C1JmA==", + "@taquito/michelson-encoder": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/michelson-encoder/-/michelson-encoder-25.0.0.tgz", + "integrity": "sha512-BWwAAwBhY3zngcMK8SkmBWDZ5aNVRnHlHvi7Xu6jnGW109yM3n9awxxCrFD8jOXPb3dly+/pulGnkv28+EvIIQ==", "requires": { - "libsodium-sumo": "^0.7.0" + "@taquito/core": "^25.0.0", + "@taquito/rpc": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "fast-json-stable-stringify": "^2.1.0" + }, + "dependencies": { + "bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==" + } } }, - "loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "@taquito/rpc": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/rpc/-/rpc-25.0.0.tgz", + "integrity": "sha512-qqV4boldGOavsM6ZzK6xky4xAEyllYDyIO5C16cvkoUzoMCCsWMpRuXa1csSrQc+0IECkkDurNLTbrMJPne9tA==", "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "@taquito/core": "^25.0.0", + "@taquito/http-utils": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2" + }, + "dependencies": { + "bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==" + } } }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "@taquito/signer": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/signer/-/signer-25.0.0.tgz", + "integrity": "sha512-UDHwWd3O0VwZjbEw0NDsoQoIhoQv+xHxb/XAKW7ruSjUT6OkH5whhHpgTZ+T2eJLPDBZLY1RH8IdSl+6ZFtUXg==", "requires": { - "brace-expansion": "^1.1.7" + "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.2.0", + "@scure/bip39": "^2.2.0", + "@stablelib/nacl": "^1.0.4", + "@taquito/core": "^25.0.0", + "@taquito/utils": "^25.0.0", + "@types/bn.js": "^5.1.5", + "bn.js": "^5.2.2", + "typedarray-to-buffer": "^4.0.0" + }, + "dependencies": { + "bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==" + } } }, - "moo": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.0.tgz", - "integrity": "sha512-AMv6iqhTEd5vT/cQlH6cammKS5ekyHhyqTRKi5zKMWl1RTyFnQ3ohPSBNSm8ySe2wlxSKwDonr9D5ZT44mdO3g==" - }, - "nearley": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.1.tgz", - "integrity": "sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==", + "@taquito/taquito": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/taquito/-/taquito-25.0.0.tgz", + "integrity": "sha512-MBNKXK1CrOlwBrI0IkjWbVcuwY238ZF8B6fuf2qA0uYp+UmVjukUJ4QCvRf+bayWzfiWBbkYP0upY6VGV3C1VQ==", "requires": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" + "@taquito/core": "^25.0.0", + "@taquito/http-utils": "^25.0.0", + "@taquito/local-forging": "^25.0.0", + "@taquito/michel-codec": "^25.0.0", + "@taquito/michelson-encoder": "^25.0.0", + "@taquito/rpc": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/utils": "^25.0.0", + "bignumber.js": "^10.0.2", + "rxjs": "^7.8.2" + }, + "dependencies": { + "bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==" + } } }, - "node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" - }, - "node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "@taquito/utils": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@taquito/utils/-/utils-25.0.0.tgz", + "integrity": "sha512-I/9hndZcx0yL6f1djPMhgxCpAopbh/E1ta5kYGs3AxDGrYKt0QpbWAvvUV7fwU9kgzHRQlm7fX8DP7zUbz7ntg==", "requires": { - "whatwg-url": "^5.0.0" + "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.2.0", + "@taquito/core": "^25.0.0", + "@types/bs58check": "^2.1.2", + "bignumber.js": "^10.0.2", + "blakejs": "^1.2.1", + "bs58check": "^3.0.1", + "buffer": "^6.0.3", + "typedarray-to-buffer": "^4.0.0" + }, + "dependencies": { + "base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" + }, + "bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==" + }, + "blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==" + }, + "bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "requires": { + "base-x": "^4.0.0" + } + }, + "bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + } + } + } } }, - "node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", "requires": { - "wrappy": "1" + "@types/node": "*" } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "@types/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-xpXaQlOIY1KoXlA/ytHGHpEIU87PJt+g9SH7nC6HdCgaBwT2IEZIwBMHbjuX6BpnfbiUMlmwqurdLDwXpcdmSA==", "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "@types/node": "*" } }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=" + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - } + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "requires": { - "safe-buffer": "^5.1.0" + "balanced-match": "^4.0.2" } }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, + "glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, - "secp256k1": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", - "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", - "requires": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - } + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } + "lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==" }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "requires": { - "safe-buffer": "~5.2.0" + "brace-expansion": "^5.0.5" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==" }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tslib": "^2.1.0" } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "typedarray-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-4.0.0.tgz", + "integrity": "sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ==" } } } diff --git a/deploy/deploy_script/package.json b/deploy/deploy_script/package.json index 41e34332..ab4177b6 100644 --- a/deploy/deploy_script/package.json +++ b/deploy/deploy_script/package.json @@ -2,15 +2,26 @@ "name": "TezFinDeploy", "version": "1.0.0", "main": "deploy.js", + "engines": { + "node": "22.16.0" + }, "dependencies": { - "conseiljs": "5.3.0", - "conseiljs-softsigner": "^5.0.4-1", - "glob": "7.2.3", - "loglevel": "^1.8.1", - "node-fetch": "^2.6.9" + "@taquito/local-forging": "^25.0.0", + "@taquito/signer": "^25.0.0", + "@taquito/taquito": "^25.0.0", + "@taquito/utils": "^25.0.0", + "glob": "^13.0.6", + "loglevel": "^1.8.1" }, "devDependencies": {}, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "check": "node -e \"require('./util').checkConnection().catch(error => { console.error('[ERROR] ' + error.message); process.exitCode = 1; })\"", + "deploy": "node deploy.js", + "deploy:e2e": "node deploy_e2e.js", + "estimate:origination": "node estimate_origination.js", + "measure:origination-size": "node measure_origination_size.js", + "prepare:deploy": "node prepare.js", + "verify:mainnet-oracle": "node verify_mainnet_oracle.js", + "test": "node --test test/*.test.js" } -} \ No newline at end of file +} diff --git a/deploy/deploy_script/prepare.js b/deploy/deploy_script/prepare.js new file mode 100644 index 00000000..51ccbe17 --- /dev/null +++ b/deploy/deploy_script/prepare.js @@ -0,0 +1,13 @@ +const { syncDeploymentOriginator, resolveDeployResultPath } = require('./util.js'); + +// Use a separate manifest file per network/deployment run. Set DEPLOY_MANIFEST to an +// explicit path (e.g. TezFinBuild/deploy_result/deploy.mainnet.json) so Previewnet and +// mainnet runs can never share or silently overwrite each other's addresses. Falls back +// to a profile-specific default derived from config.json's networkProfile (see +// resolveDeployResultPath in util.js) when DEPLOY_MANIFEST isn't set. +const deployResultPath = resolveDeployResultPath(); + +syncDeploymentOriginator(deployResultPath).catch((error) => { + console.error(`[ERROR] Deployment preparation failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/deploy/deploy_script/test/deploy_guards.test.js b/deploy/deploy_script/test/deploy_guards.test.js new file mode 100644 index 00000000..04a0d387 --- /dev/null +++ b/deploy/deploy_script/test/deploy_guards.test.js @@ -0,0 +1,441 @@ +// Offline/unit tests for the deployment safety guards added per the PR #455 review +// ("Required Deployment Tests" checklist): chain-id rejection, manifest path +// resolution, on-chain code/storage verification comparison logic, and mainnet +// preflight canonical-address checks. These deliberately avoid any real network +// access (no Tezos RPC calls) so they run fast and deterministically in CI. +// +// Run with: node --test deploy/deploy_script/test/deploy_guards.test.js + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const { + checkChainIdMatch, + micheline_equal, + michelineScriptEqual, + decodeTezosAddressBytes, + extractAddressBindings, + diffAddressBindings, + resolveDeployResultPath, + verifyExistingContract, + enforceDeploymentPreflight, +} = require('../util.js'); +const { checkNetworkExpectation, MAINNET_CHAIN_IDS } = require('../assert_network.js'); +const { findMissingCanonicalKeys, verifyAgainstAllowlist, REQUIRED_CANONICAL_KEYS, VETTED_MAINNET_ADDRESSES } = require('../mainnet_preflight.js'); +const { parsePriceResult } = require('../verify_mainnet_oracle.js'); + +const CUSDT_COMPILED_ADDRESSES = [ + { string: 'KT1Wq7uJeiXXociunW4LqQZzdNvM7QYbtVEN' }, + { string: 'KT1N8XU2pe51z22aMbRQya9YhPmCytyX2Met' }, + { string: 'KT1WWKLMc2aAanNxvYR3GcLpArTnYG6sbzzp' }, + { string: 'KT1QT1P8rHnoirQquBV1cmTaWivgGXFxhYax' }, +]; +const CUSDT_RPC_ADDRESSES = [ + { bytes: '01f409912cd853b330c9fbe60315b2e7301ef9194b00' }, + { bytes: '01949b2d456ed3104815e3c281a570c77e063b8eba00' }, + { bytes: '01f07b34ea1003b885b8155095a2573a7f6395a66500' }, + { bytes: '01ae0a37ee2338f34330c1d46c35ceea4aa7c1fcc100' }, +]; + +function createCUSDtStorage([administrator, comptroller, underlying, irm]) { + return { + prim: 'Pair', + args: [{ + prim: 'Pair', + args: [{ + prim: 'Pair', + args: [ + { + prim: 'Pair', + args: [ + { prim: 'Pair', args: [{ int: '0' }, { prim: 'Pair', args: [[], administrator] }] }, + { int: '1000000000000000000' }, + ], + }, + { + prim: 'Pair', + args: [ + { prim: 'Pair', args: [{ int: '1075' }, { prim: 'Pair', args: [comptroller, { int: '0' }] }] }, + { prim: 'Pair', args: [underlying, { int: '1000000000000000000' }] }, + ], + }, + ], + }, { + prim: 'Pair', + args: [{ prim: 'Pair', args: [irm, []] }, { int: '0' }], + }], + }, { int: '0' }], + }; +} + +test('checkChainIdMatch: accepts matching chain ids', () => { + assert.doesNotThrow(() => checkChainIdMatch('NetXY2oPPzkxUW1', 'NetXY2oPPzkxUW1', 'test manifest')); +}); + +test('checkChainIdMatch: accepts when no expected chain id is set (fresh manifest)', () => { + assert.doesNotThrow(() => checkChainIdMatch(undefined, 'NetXY2oPPzkxUW1', 'test manifest')); +}); + +test('checkChainIdMatch: rejects mismatched chain ids', () => { + assert.throws( + () => checkChainIdMatch('NetXdQprcVkpaWU', 'NetXY2oPPzkxUW1', 'Deploy manifest deploy.mainnet.json'), + /chain NetXdQprcVkpaWU.*chain NetXY2oPPzkxUW1/s, + ); +}); + +test('assertNetwork guard: rejects an unknown profile', () => { + assert.throws( + () => checkNetworkExpectation('staging', undefined, 'NetXY2oPPzkxUW1', 'https://node.example'), + /Unknown network profile "staging"/, + ); +}); + +test('assertNetwork guard: rejects config.json declaring a different profile than requested', () => { + assert.throws( + () => checkNetworkExpectation('mainnet', 'previewnet', 'NetXdQprcVkpaWU', 'https://node.example'), + /declares networkProfile "previewnet"/, + ); +}); + +test('assertNetwork guard: rejects running the mainnet script against a non-mainnet chain id', () => { + assert.throws( + () => checkNetworkExpectation('mainnet', 'mainnet', 'NetXY2oPPzkxUW1', 'https://node.example'), + /not a known mainnet chain id/, + ); +}); + +test('assertNetwork guard: rejects running the previewnet script against a known mainnet chain id', () => { + const [mainnetChainId] = MAINNET_CHAIN_IDS; + assert.throws( + () => checkNetworkExpectation('previewnet', 'previewnet', mainnetChainId, 'https://node.example'), + /Refusing to run the Previewnet deploy script against mainnet/, + ); +}); + +test('assertNetwork guard: accepts a correctly-matched mainnet chain id', () => { + const [mainnetChainId] = MAINNET_CHAIN_IDS; + assert.doesNotThrow(() => checkNetworkExpectation('mainnet', 'mainnet', mainnetChainId, 'https://node.example')); +}); + +test('micheline_equal: treats differently-ordered object keys as equal', () => { + const a = { prim: 'Pair', args: [{ int: '1' }, { string: 'tz1abc' }] }; + const b = { args: [{ int: '1' }, { string: 'tz1abc' }], prim: 'Pair' }; + assert.equal(micheline_equal(a, b), true); +}); + +test('micheline_equal: detects a real difference in embedded values', () => { + const a = { prim: 'Pair', args: [{ int: '1' }] }; + const b = { prim: 'Pair', args: [{ int: '2' }] }; + assert.equal(micheline_equal(a, b), false); +}); + +test('michelineScriptEqual: ignores top-level Michelson section order only', () => { + const parameter = { prim: 'parameter', args: [{ prim: 'unit' }] }; + const storage = { prim: 'storage', args: [{ prim: 'unit' }] }; + const code = { prim: 'code', args: [[{ prim: 'CAR' }, { prim: 'NIL', args: [{ prim: 'operation' }] }]] }; + const view = { prim: 'view', args: [{ string: 'get' }, { prim: 'unit' }, { prim: 'unit' }, []] }; + + assert.equal(michelineScriptEqual([storage, parameter, code, view], [view, parameter, storage, code]), true); + assert.equal( + michelineScriptEqual( + [storage, parameter, code, view], + [view, parameter, storage, { ...code, args: [[{ prim: 'NIL', args: [{ prim: 'operation' }] }, { prim: 'CAR' }]] }], + ), + false, + ); +}); + +test('decodeTezosAddressBytes: decodes optimized tz1/tz2/tz3/tz4 and KT1 encodings', () => { + const vectors = [ + ['00001111111111111111111111111111111111111111', 'tz1MCGdC9qYbSjtWEbup9i17WkohvzwCm2HV'], + ['00011111111111111111111111111111111111111111', 'tz29sUbQkQxxNVXNWmxepLyN4L4iStKf9x8Y'], + ['00021111111111111111111111111111111111111111', 'tz3MtHYjeH6Vm7yfw32upJRjsgxEDiVdgA85'], + ['00031111111111111111111111111111111111111111', 'tz4AZVWxErWrgscYDD5kUwPzRGDEjbwhTeLz'], + ['01222222222222222222222222222222222222222200', 'KT1BhFRuvKL9E8ggxycsHDf8qS42HLvCrXYr'], + ]; + + for (const [encoded, address] of vectors) { + assert.equal(decodeTezosAddressBytes(encoded), address); + } +}); + +test('decodeTezosAddressBytes: ignores malformed and unsupported encodings', () => { + assert.equal(decodeTezosAddressBytes('0004' + '11'.repeat(20)), undefined); + assert.equal(decodeTezosAddressBytes('01' + '22'.repeat(20) + '01'), undefined); + assert.equal(decodeTezosAddressBytes('00'), undefined); + assert.equal(decodeTezosAddressBytes('not-hex'), undefined); +}); + +test('extractAddressBindings: records tz1/KT1 strings at their Micheline paths', () => { + const tz1 = 'tz1VLnrVYrMtLHRUfLV594uvzSthZ5w7wXqE'; + const kt1 = 'KT1WvzYHCNBvDSdwafTHv7nJ1dWmZ8GCYuuC'; + const storage = { + prim: 'Pair', + args: [ + { string: tz1 }, + { prim: 'Pair', args: [{ string: kt1 }, { int: '42' }] }, + ], + }; + const addresses = extractAddressBindings(storage); + assert.equal(addresses.get('$.args[0]'), tz1); + assert.equal(addresses.get('$.args[1].args[0]'), kt1); + assert.equal(addresses.size, 2); +}); + +test('extractAddressBindings: normalizes string and bytes addresses to identical paths', () => { + const stringStorage = { + prim: 'Pair', + args: [ + { string: 'tz1MCGdC9qYbSjtWEbup9i17WkohvzwCm2HV' }, + { string: 'KT1BhFRuvKL9E8ggxycsHDf8qS42HLvCrXYr' }, + ], + }; + const bytesStorage = { + prim: 'Pair', + args: [ + { bytes: '00001111111111111111111111111111111111111111' }, + { bytes: '01222222222222222222222222222222222222222200' }, + ], + }; + + assert.deepEqual(extractAddressBindings(bytesStorage), extractAddressBindings(stringStorage)); +}); + +test('extractAddressBindings: matches compiled CUSDt strings to Previewnet RPC bytes', () => { + const compiledStorage = createCUSDtStorage(CUSDT_COMPILED_ADDRESSES); + const rpcStorage = createCUSDtStorage(CUSDT_RPC_ADDRESSES); + + assert.deepEqual(extractAddressBindings(rpcStorage), extractAddressBindings(compiledStorage)); +}); + +test('diffAddressBindings: reports addresses assigned to different roles', () => { + const expected = new Map([['$.admin', 'tz1Admin'], ['$.oracle', 'KT1Oracle']]); + const actual = new Map([['$.admin', 'KT1Oracle'], ['$.oracle', 'tz1Admin']]); + assert.deepEqual(diffAddressBindings(expected, actual), [ + { path: '$.admin', expected: 'tz1Admin', actual: 'KT1Oracle' }, + { path: '$.oracle', expected: 'KT1Oracle', actual: 'tz1Admin' }, + ]); +}); + +test('verifyExistingContract: rejects addresses swapped between immutable roles', async () => { + const expectedCode = [{ prim: 'parameter', args: [{ prim: 'unit' }] }]; + const administrator = 'tz1VLnrVYrMtLHRUfLV594uvzSthZ5w7wXqE'; + const comptroller = 'KT1WvzYHCNBvDSdwafTHv7nJ1dWmZ8GCYuuC'; + const expectedStorage = { + prim: 'Pair', + args: [{ string: administrator }, { string: comptroller }], + }; + const tezos = { + rpc: { + getScript: async () => ({ code: expectedCode }), + getStorage: async () => ({ + prim: 'Pair', + args: [{ string: comptroller }, { string: administrator }], + }), + }, + }; + + await assert.rejects( + verifyExistingContract(tezos, 'KT1ExistingMarketAddress', expectedCode, expectedStorage, 'CUSDt'), + /different role-aware address bindings/, + ); +}); + +test('verifyExistingContract: accepts matching CUSDt string/bytes wiring', async () => { + const expectedCode = [{ prim: 'parameter', args: [{ prim: 'unit' }] }]; + const tezos = { + rpc: { + getScript: async () => ({ code: expectedCode }), + getStorage: async () => createCUSDtStorage(CUSDT_RPC_ADDRESSES), + }, + }; + + await assert.doesNotReject( + verifyExistingContract( + tezos, + 'KT1Cugpw5mGTQydYt2dCw2CUxsPsnGSdt7R9', + expectedCode, + createCUSDtStorage(CUSDT_COMPILED_ADDRESSES), + 'CUSDt', + ), + ); +}); + +test('verifyExistingContract: rejects every administrator/comptroller/underlying/IRM swap', async () => { + const expectedCode = [{ prim: 'parameter', args: [{ prim: 'unit' }] }]; + const roles = ['administrator', 'comptroller', 'underlying', 'IRM']; + + for (let left = 0; left < roles.length; left++) { + for (let right = left + 1; right < roles.length; right++) { + const swapped = [...CUSDT_RPC_ADDRESSES]; + [swapped[left], swapped[right]] = [swapped[right], swapped[left]]; + const tezos = { + rpc: { + getScript: async () => ({ code: expectedCode }), + getStorage: async () => createCUSDtStorage(swapped), + }, + }; + + await assert.rejects( + verifyExistingContract( + tezos, + 'KT1Cugpw5mGTQydYt2dCw2CUxsPsnGSdt7R9', + expectedCode, + createCUSDtStorage(CUSDT_COMPILED_ADDRESSES), + 'CUSDt', + ), + /different role-aware address bindings/, + ); + } + } +}); + +test('verifyExistingContract: rejects an IRM with mismatched immutable rate parameters', async () => { + const expectedCode = [{ prim: 'parameter', args: [{ prim: 'unit' }] }]; + const expectedStorage = { + prim: 'Pair', + args: [{ int: '100' }, { prim: 'Pair', args: [{ int: '200' }, { int: '300' }] }], + }; + const tezos = { + rpc: { + getScript: async () => ({ code: expectedCode }), + getStorage: async () => ({ + prim: 'Pair', + args: [{ int: '100' }, { prim: 'Pair', args: [{ int: '201' }, { int: '300' }] }], + }), + }, + }; + + await assert.rejects( + verifyExistingContract(tezos, 'KT1ExistingIrmAddress', expectedCode, expectedStorage, 'CFA2_IRM'), + /immutable IRM parameters do not match/, + ); +}); + +test('resolveDeployResultPath: DEPLOY_MANIFEST env var always wins', () => { + const previous = process.env.DEPLOY_MANIFEST; + process.env.DEPLOY_MANIFEST = '/tmp/custom-manifest.json'; + try { + assert.equal(resolveDeployResultPath(), path.resolve('/tmp/custom-manifest.json')); + } finally { + if (previous === undefined) { + delete process.env.DEPLOY_MANIFEST; + } else { + process.env.DEPLOY_MANIFEST = previous; + } + } +}); + +test('mainnetPreflight: findMissingCanonicalKeys flags every missing required key', () => { + const missing = findMissingCanonicalKeys({ PriceOracle: 'KT1Oracle' }); + assert.deepEqual(missing, REQUIRED_CANONICAL_KEYS.filter((key) => key !== 'PriceOracle')); +}); + +test('raw deployment path: mainnet requires preflight', async () => { + let receivedPath; + let oracleManifestPath; + await enforceDeploymentPreflight('/tmp/deploy.mainnet.json', 'mainnet', 'NetXdQprcVkpaWU', async (manifestPath) => { + receivedPath = manifestPath; + }, async (manifestPath) => { + oracleManifestPath = manifestPath; + }); + assert.equal(receivedPath, '/tmp/deploy.mainnet.json'); + assert.equal(oracleManifestPath, '/tmp/deploy.mainnet.json'); +}); + +test('raw deployment path: mainnet preflight failure blocks deployment', async () => { + await assert.rejects( + enforceDeploymentPreflight('/tmp/deploy.mainnet.json', 'previewnet', 'NetXdQprcVkpaWU', async () => { + throw new Error('MAINNET_DEPLOY_CONFIRM is required'); + }, async () => {}), + /MAINNET_DEPLOY_CONFIRM is required/, + ); +}); + +test('raw deployment path: oracle verification failure blocks deployment', async () => { + await assert.rejects( + enforceDeploymentPreflight( + '/tmp/deploy.mainnet.json', + 'mainnet', + 'NetXdQprcVkpaWU', + async () => {}, + async () => { + throw new Error('XTZUSDT price is stale'); + }, + ), + /XTZUSDT price is stale/, + ); +}); + +test('raw deployment path: previewnet does not run mainnet preflights', async () => { + await enforceDeploymentPreflight( + '/tmp/deploy.previewnet.json', + 'previewnet', + 'NetXnHfVqm9iesp', + async () => { + throw new Error('mainnet preflight should not run'); + }, + async () => { + throw new Error('mainnet oracle verification should not run'); + }, + ); +}); + +test('mainnetPreflight: findMissingCanonicalKeys reports nothing when all keys present', () => { + const fullManifest = Object.fromEntries(REQUIRED_CANONICAL_KEYS.map((key) => [key, `KT1${key}`])); + assert.deepEqual(findMissingCanonicalKeys(fullManifest), []); +}); + +test('mainnetPreflight: verifyAgainstAllowlist rejects a missing required allowlist entry', () => { + const vettedOracle = VETTED_MAINNET_ADDRESSES.PriceOracle; + delete VETTED_MAINNET_ADDRESSES.PriceOracle; + try { + assert.throws( + () => verifyAgainstAllowlist('PriceOracle', 'KT1SomeAddress'), + /No vetted mainnet address is configured for required key "PriceOracle"/, + ); + } finally { + VETTED_MAINNET_ADDRESSES.PriceOracle = vettedOracle; + } +}); + +test('mainnetPreflight: verifyAgainstAllowlist rejects an address that does not match a configured vetted entry', () => { + const vettedOracle = VETTED_MAINNET_ADDRESSES.PriceOracle; + VETTED_MAINNET_ADDRESSES.PriceOracle = 'KT1VettedOracleAddressXXXXXXXXXXXXX'; + try { + assert.throws( + () => verifyAgainstAllowlist('PriceOracle', 'KT1DifferentOracleAddressXXXXXXXXX'), + /vetted mainnet address is KT1VettedOracleAddressXXXXXXXXXXXXX/, + ); + assert.doesNotThrow(() => verifyAgainstAllowlist('PriceOracle', 'KT1VettedOracleAddressXXXXXXXXXXXXX')); + } finally { + VETTED_MAINNET_ADDRESSES.PriceOracle = vettedOracle; + } +}); + +test('mainnet oracle guard: accepts a current nonzero price', () => { + const response = { data: { args: [{ int: '226300' }, { int: '1784510000' }] } }; + assert.deepEqual(parsePriceResult('XTZUSDT', response, 1784510030, 300), { + asset: 'XTZUSDT', price: 226300, timestamp: 1784510000, rawTimestamp: 1784510000, ageSeconds: 30, + }); +}); + +test('mainnet oracle guard: rejects milliseconds, future timestamps, stale prices, and zero prices', () => { + assert.throws( + () => parsePriceResult('XTZUSDT', { data: { args: [{ int: '226300' }, { int: '1784510000000' }] } }, 1784510030, 300), + /Unix seconds, not milliseconds/, + ); + assert.throws( + () => parsePriceResult('XTZUSDT', { data: { args: [{ int: '226300' }, { int: '1784510031' }] } }, 1784510030, 300), + /ahead of the mainnet head/, + ); + assert.throws( + () => parsePriceResult('TZBTCUSDT', { data: { args: [{ int: '0' }, { int: '1784510000' }] } }, 1784510030, 300), + /invalid or zero price/, + ); + assert.throws( + () => parsePriceResult('TZBTCUSDT', { data: { args: [{ int: '1' }, { int: '1784400000' }] } }, 1784510030, 300), + /price is stale/, + ); +}); diff --git a/deploy/deploy_script/test/origination_size.test.js b/deploy/deploy_script/test/origination_size.test.js new file mode 100644 index 00000000..8b89ef0c --- /dev/null +++ b/deploy/deploy_script/test/origination_size.test.js @@ -0,0 +1,111 @@ +// Offline tests for measure_origination_size.js, which is what the CI size gate +// (deploy/compile_targets/tests/test_operation_size.py) uses to decide whether a contract +// can actually be originated. These pin the two properties that make the measurement +// trustworthy: it accounts for the whole operation rather than just code+storage, and it +// tracks contract size monotonically. No network access. +// +// Run with: node --test deploy/deploy_script/test/origination_size.test.js + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { + measureOriginationSize, + resolveArtifactPaths, + SIGNATURE_BYTES, +} = require('../measure_origination_size.js'); + +// A minimal but real contract: `parameter nat; storage nat; code { CDR; NIL operation; PAIR }`. +function buildContract(extraStorageEntries) { + return [ + { prim: 'parameter', args: [{ prim: 'nat' }] }, + { prim: 'storage', args: [{ prim: 'nat' }] }, + { + prim: 'code', + args: [[{ prim: 'CDR' }, { prim: 'NIL', args: [{ prim: 'operation' }] }, { prim: 'PAIR' }]], + }, + ...extraStorageEntries, + ]; +} + +function writeArtifacts(dir, contract, storage) { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'step_000_cont_0_contract.json'), JSON.stringify(contract)); + fs.writeFileSync(path.join(dir, 'step_000_cont_0_storage.json'), JSON.stringify(storage)); + return dir; +} + +// Awaits run() before cleaning up, so an async body cannot have the directory deleted out +// from under it at its first suspension point. +async function withTempDir(run) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'origination_size_test_')); + try { + return await run(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +test('reported opSize is the forged operation plus the signature', async () => { + await withTempDir(async (dir) => { + const artifacts = writeArtifacts(dir, buildContract([]), { int: '0' }); + const measured = await measureOriginationSize(resolveArtifactPaths(artifacts)); + + assert.equal(measured.signatureBytes, SIGNATURE_BYTES); + assert.equal(measured.opSize, measured.forgedBytes + SIGNATURE_BYTES); + }); +}); + +test('opSize accounts for the operation envelope, not just the script', async () => { + // This is the whole point of the check: gating on code+storage alone under-reports what + // has to fit under the protocol's 32768-byte limit. A tiny contract's script is a + // handful of bytes, yet the operation carrying it must also encode the branch (32B), + // source (21B), the four zarith limits, and the signature (64B). + await withTempDir(async (dir) => { + const artifacts = writeArtifacts(dir, buildContract([]), { int: '0' }); + const measured = await measureOriginationSize(resolveArtifactPaths(artifacts)); + + const branchAndSourceBytes = 32 + 21; + assert.ok( + measured.forgedBytes > branchAndSourceBytes, + `forged operation (${measured.forgedBytes}B) must exceed branch+source framing alone`, + ); + assert.ok(measured.opSize > measured.forgedBytes, 'opSize must add the signature'); + }); +}); + +test('a larger contract measures larger', async () => { + await withTempDir(async (dir) => { + const small = writeArtifacts(path.join(dir, 'small'), buildContract([]), { int: '0' }); + // Views are part of the contract script, so adding one must increase the measurement. + const big = writeArtifacts( + path.join(dir, 'big'), + buildContract([{ + prim: 'view', + args: [ + { string: 'someView' }, + { prim: 'unit' }, + { prim: 'nat' }, + [{ prim: 'CDR' }], + ], + }]), + { int: '0' }, + ); + + const smallMeasured = await measureOriginationSize(resolveArtifactPaths(small)); + const bigMeasured = await measureOriginationSize(resolveArtifactPaths(big)); + assert.ok( + bigMeasured.opSize > smallMeasured.opSize, + `expected the contract with an extra view to measure larger, got ${bigMeasured.opSize} <= ${smallMeasured.opSize}`, + ); + }); +}); + +test('resolveArtifactPaths rejects a directory without SmartPy output', async () => { + await withTempDir((dir) => { + assert.throws(() => resolveArtifactPaths(dir), /does not look like a SmartPy output directory/); + }); +}); diff --git a/deploy/deploy_script/util.js b/deploy/deploy_script/util.js index d75f6a08..bd3bd680 100644 --- a/deploy/deploy_script/util.js +++ b/deploy/deploy_script/util.js @@ -1,52 +1,252 @@ const fs = require('fs'); - -var os = require("os"); +const os = require('os'); const path = require('path'); -const fetch = require('node-fetch'); -const log = require('loglevel'); -const conseiljs = require('conseiljs'); -const conseiljssoftsigner = require('conseiljs-softsigner'); const glob = require('glob'); +const { InMemorySigner } = require('@taquito/signer'); +const { TezosToolkit } = require('@taquito/taquito'); +const { encodeAddress, encodeKeyHash } = require('@taquito/utils'); -const { registerFetch, registerLogger } = conseiljs; -const { KeyStoreUtils } = conseiljssoftsigner; +const configPath = path.join(__dirname, 'config.json'); +const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); +// Single source of truth for the manifest path so prepare/deploy/verify-oracle/preflight +// can never silently disagree about which file they're reading/writing. Resolution order: +// 1. DEPLOY_MANIFEST env var, if set (explicit override always wins). +// 2. A profile-specific default derived from config.json's networkProfile, so a +// mainnet config.json never defaults to the Previewnet manifest file (or vice versa). +function resolveDeployResultPath() { + if (process.env.DEPLOY_MANIFEST) { + return path.resolve(process.env.DEPLOY_MANIFEST); + } + const fileName = config.networkProfile === 'mainnet' ? 'deploy.mainnet.json' : 'deploy.json'; + return path.join(__dirname, '../../TezFinBuild/deploy_result', fileName); +} -//init conseil -const logger = log.getLogger('conseiljs'); -logger.setLevel(log.levels.ERROR, false); -registerLogger(logger); -registerFetch(fetch); +function getRequiredEnv(name) { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is required. Add it to your shell environment; do not commit private keys.`); + } + return value; +} +async function initAccount() { + if (process.env.TEZOS_PRIVATE_KEY) { + return InMemorySigner.fromSecretKey(process.env.TEZOS_PRIVATE_KEY.trim()); + } + const mnemonic = process.env.TEZOS_MNEMONIC; + if (!mnemonic) { + getRequiredEnv('TEZOS_PRIVATE_KEY'); + } -const config = JSON.parse(fs.readFileSync("deploy/deploy_script/config.json", 'utf8')); + const fundraiserEmail = process.env.TEZOS_FUNDS_EMAIL; + const fundraiserPassword = process.env.TEZOS_FUNDS_PASSWORD; + if (fundraiserEmail || fundraiserPassword) { + if (!fundraiserEmail || !fundraiserPassword) { + throw new Error('Set both TEZOS_FUNDS_EMAIL and TEZOS_FUNDS_PASSWORD for a fundraiser account.'); + } + return InMemorySigner.fromFundraiser(fundraiserEmail, fundraiserPassword, mnemonic); + } -const tezosNode = config['tezosNode'] + return InMemorySigner.fromMnemonic({ + mnemonic, + password: process.env.TEZOS_MNEMONIC_PASSWORD || '', + derivationPath: process.env.TEZOS_DERIVATION_PATH || "44'/1729'/0'/0'", + }); +} -let faucetAccount = config['originator'] -let keystore; +function parseMichelsonFile(filePath, kind) { + let value; + try { + value = JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + throw new Error(`Could not parse ${kind} JSON at ${filePath}: ${error.message}`); + } + if (kind === 'contract') { + assertValidMichelsonCode(value, filePath); + } + return value; +} + +// SmartPy can still write a *.json artifact after a compile-time type error, with +// `{ "prim": "ERROR", ... }` nodes in place of real Michelson. The node then rejects +// origination with an opaque "unexpected string value ERROR" parse failure. Catch that +// here so the deploy fails before fee estimation / injection. +function assertValidMichelsonCode(node, filePath) { + const stack = [node]; + while (stack.length) { + const current = stack.pop(); + if (Array.isArray(current)) { + for (const child of current) stack.push(child); + continue; + } + if (!current || typeof current !== 'object') continue; + if (current.prim === 'ERROR') { + throw new Error( + `${filePath} contains invalid SmartPy output (prim "ERROR"). Recompile the ` + + `source and fix any SmartPy warnings such as "Missing type" / "unknown type ` + + `variable" before deploying.`, + ); + } + if (current.args) stack.push(current.args); + } +} +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} -async function initAccount() { - keystore = await KeyStoreUtils.restoreIdentityFromFundraiser(faucetAccount['mnemonic'].join(' '), - faucetAccount['email'], faucetAccount['password'], faucetAccount['pkh']); +function isTransientRpcError(error) { + const message = String(error && error.message ? error.message : error); + return /504|502|503|408|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|Gateway Time-out|Bad Gateway|Service Unavailable/i.test( + message, + ); } -async function deployMichelsonContract(contract, initialStorage) { - const signer = await conseiljssoftsigner.SoftSigner.createSigner(conseiljs.TezosMessageUtils.writeKeyWithHint(keystore.secretKey, 'edsk')); - const head = await conseiljs.TezosNodeReader.getBlockHead(tezosNode) - const result = await conseiljs.TezosNodeWriter.sendContractOriginationOperation( - tezosNode, signer, keystore, 0, undefined, 100000, 60000, 400000, - contract, initialStorage, conseiljs.TezosParameterFormat.Micheline) +async function rpcGetJson(pathname) { + const base = config.tezosNode.replace(/\/$/, ''); + const response = await fetch(`${base}${pathname}`); + if (!response.ok) { + const body = await response.text(); + throw new Error(`Http error response: (${response.status}) ${body}`); + } + return response.json(); +} - const groupid = result['operationGroupID'].replace(/\"/g, '').replace(/\n/, ''); // clean up RPC output - console.log(`[Info] Injected operation group id ${groupid}`); +async function findOriginationAddress(opHash, { + maxAttempts = Number(process.env.TEZOS_CONFIRMATION_ATTEMPTS || 90), + pollIntervalMs = Number(process.env.TEZOS_CONFIRMATION_POLL_MS || 3000), + lookbackBlocks = Number(process.env.TEZOS_CONFIRMATION_LOOKBACK || 40), +} = {}) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const header = await rpcGetJson('/chains/main/blocks/head/header'); + for (let delta = 0; delta < lookbackBlocks; delta++) { + const level = header.level - delta; + if (level < 1) { + break; + } + const block = await rpcGetJson(`/chains/main/blocks/${level}`); + for (const passOps of block.operations || []) { + for (const op of passOps) { + if (op.hash !== opHash) { + continue; + } + for (const content of op.contents || []) { + const result = content.metadata && content.metadata.operation_result; + if (!result) { + continue; + } + if (result.status === 'applied' && result.originated_contracts?.length) { + return result.originated_contracts[0]; + } + if (result.status && result.status !== 'applied') { + throw new Error( + `Operation ${opHash} finished with status ${result.status}`, + ); + } + } + } + } + } + console.log( + `[INFO] Waiting for ${opHash} to appear in a block (attempt ${attempt}/${maxAttempts})`, + ); + } catch (error) { + if (!isTransientRpcError(error)) { + throw error; + } + console.log( + `[WARN] Transient RPC error confirming ${opHash} (attempt ${attempt}/${maxAttempts}): ${error.message}`, + ); + } + await sleep(pollIntervalMs); + } + throw new Error(`Timed out waiting for confirmation of ${opHash}`); +} - const conseilResult = await conseiljs.TezosNodeReader.awaitOperationConfirmation(tezosNode, head.header.level - 1, groupid, 6).then(res => { if (res['contents'][0]['metadata']['operation_result']['status'] === "applied") return res; else throw new Error("operation status not applied"); }).catch((error) => { console.log(error) }); - console.log(`[Info] Originated contract at ${conseilResult['contents'][0]['metadata']['operation_result']['originated_contracts'][0]}`); +async function deployMichelsonContract(tezos, contract, initialStorage, name) { + const feeSafetyMultiplier = Number( + process.env.TEZOS_FEE_SAFETY_MULTIPLIER || config.feeSafetyMultiplier || 1.2, + ); + if (!Number.isFinite(feeSafetyMultiplier) || feeSafetyMultiplier < 1) { + throw new Error('TEZOS_FEE_SAFETY_MULTIPLIER must be a number greater than or equal to 1.'); + } - return conseilResult['contents'][0]['metadata']['operation_result']['originated_contracts'][0]; + const params = { + balance: '0', + code: contract, + init: initialStorage, + }; + const estimate = await tezos.estimate.originate(params); + const fee = Math.ceil(estimate.suggestedFeeMutez * feeSafetyMultiplier); + console.log( + `[INFO] ${name} fee estimate: ${estimate.suggestedFeeMutez} mutez; using ${fee} mutez`, + ); + const operation = await tezos.contract.originate({ + ...params, + fee, + gasLimit: estimate.gasLimit, + storageLimit: estimate.storageLimit, + }); + console.log(`[INFO] Injected ${name}: ${operation.hash}`); + + let address; + try { + // Keep Taquito's waiter short; Previewnet RPC often 504s on long polls. + await operation.confirmation(1, 45); + address = (await operation.contract()).address; + } catch (error) { + console.log( + `[WARN] Taquito confirmation failed for ${name} (${error.message}); falling back to RPC block scan`, + ); + address = await findOriginationAddress(operation.hash); + } + + console.log(`[INFO] Originated ${name}: ${address}`); + return address; +} + +async function createTezosClient() { + const tezos = new TezosToolkit(config.tezosNode); + const signer = await initAccount(); + tezos.setProvider({ signer }); + + const chainId = await tezos.rpc.getChainId(); + checkChainIdMatch(config.chainId, chainId, 'config.json chainId'); + const publicKeyHash = await signer.publicKeyHash(); + if (config.originator?.pkh && config.originator.pkh !== publicKeyHash) { + throw new Error(`Configured originator ${config.originator.pkh} does not match signing key ${publicKeyHash}`); + } + return { tezos, publicKeyHash, chainId }; +} + +async function checkConnection() { + const { tezos, publicKeyHash, chainId } = await createTezosClient(); + const balance = await tezos.tz.getBalance(publicKeyHash); + console.log(`[INFO] Connected to ${config.tezosNode} (${chainId})`); + console.log(`[INFO] Signer ${publicKeyHash} has ${balance.toString()} mutez available`); +} + +async function syncDeploymentOriginator(deployResultPath) { + const { publicKeyHash, chainId } = await createTezosClient(); + const deployResult = readDeployResult(deployResultPath); + if (deployResult.chainId) { + try { + checkChainIdMatch(deployResult.chainId, chainId, `Deploy manifest ${deployResultPath}`); + } catch (error) { + throw new Error( + `${error.message} Refusing to reuse this manifest; start a fresh manifest (e.g. delete ` + + `or rename the file) before preparing a deployment for this network.`, + ); + } + } + deployResult.OriginatorAddress = publicKeyHash; + deployResult.chainId = chainId; + deployResult.network = config.tezosNode; + writeDeployResult(deployResultPath, JSON.stringify(deployResult, null, ' ')); + console.log(`[INFO] Set OriginatorAddress to ${publicKeyHash} in ${deployResultPath}`); } function getDirectories(path) { @@ -56,20 +256,19 @@ function getDirectories(path) { } function getMichelsonCode(directory) { - pattern = path.join(directory, "*contract.json") - return findFirstFile(pattern) + return findFirstFile(path.join(directory, "*contract.json"), 'contract') } function getMichelsonStorage(directory) { - pattern = path.join(directory, "*storage.json") - return findFirstFile(pattern) + return findFirstFile(path.join(directory, "*storage.json"), 'storage') } -function findFirstFile(pattern) { - const files = glob(pattern, { sync: true }) - filePath = files[0] - let fileRawData = fs.readFileSync(filePath, 'utf8'); - return fileRawData +function findFirstFile(pattern, kind) { + const files = glob.sync(pattern); + if (files.length !== 1) { + throw new Error(`Expected one ${kind} file matching ${pattern}, found ${files.length}`); + } + return parseMichelsonFile(files[0], kind); } function readDeployResult(jsonPath) { @@ -84,56 +283,299 @@ function writeDeployResult(jsonPath, data) { fs.writeFileSync(jsonPath, data + os.EOL) } -async function run() { - console.log(`[INFO] Log level ${log.getLevel()}`); - compiledContractsPath = path.join(__dirname, "../../TezFinBuild/compiled_contracts"); - const directories = getDirectories(compiledContractsPath); - console.log(directories); - await initAccount(); - deployResultPath = path.join(__dirname, "../../TezFinBuild/deploy_result/deploy.json") - jsonDeployResult = readDeployResult(deployResultPath); +async function fetchOnChainScript(tezos, address) { + return tezos.rpc.getScript(address); +} + +async function fetchOnChainStorage(tezos, address) { + return tezos.rpc.getStorage(address); +} + +// Micheline JSON from the RPC and from SmartPy's compiled output can differ in key +// order (and, occasionally, annotation order) without being semantically different. +// Canonicalize both sides the same way before comparing so that a legitimate resume +// isn't falsely rejected due to formatting differences alone. +function canonicalizeMicheline(node) { + if (Array.isArray(node)) { + return node.map(canonicalizeMicheline); + } + if (node && typeof node === 'object') { + const sortedKeys = Object.keys(node).sort(); + const result = {}; + for (const key of sortedKeys) { + const value = node[key]; + result[key] = key === 'annots' && Array.isArray(value) + ? [...value].sort() + : canonicalizeMicheline(value); + } + return result; + } + return node; +} + +function micheline_equal(a, b) { + return JSON.stringify(canonicalizeMicheline(a)) === JSON.stringify(canonicalizeMicheline(b)); +} + +function michelineScriptEqual(a, b) { + if (!Array.isArray(a) || !Array.isArray(b)) { + return false; + } + const sectionKey = (section) => + `${section.prim}:${section.prim === 'view' ? section.args?.[0]?.string || '' : ''}`; + const sortSections = (sections) => + [...sections].sort((left, right) => sectionKey(left).localeCompare(sectionKey(right))); + return micheline_equal(sortSections(a), sortSections(b)); +} + +const TEZOS_ADDRESS_PATTERN = /^(tz1|tz2|tz3|tz4|KT1)[1-9A-HJ-NP-Za-km-z]{33}$/; + +function decodeTezosAddressBytes(value) { + if (typeof value !== 'string' || !/^[0-9a-fA-F]{44}$/.test(value)) { + return undefined; + } + + const bytes = value.toLowerCase(); + if (/^00(00|01|02|03)[0-9a-f]{40}$/.test(bytes)) { + return encodeKeyHash(bytes.slice(2)); + } + if (/^01[0-9a-f]{40}00$/.test(bytes)) { + return encodeAddress(bytes); + } + return undefined; +} + +// Record each embedded address at its exact Micheline path. Paths preserve the role of +// administrator, oracle, underlying token, comptroller, and IRM addresses, so swapping +// two addresses cannot pass merely because the unordered set of values is unchanged. +// Mutable non-address storage is intentionally ignored; immutable IRMs are compared in full. +function extractAddressBindings(node, path = '$', out = new Map()) { + if (Array.isArray(node)) { + for (const [index, item] of node.entries()) { + extractAddressBindings(item, `${path}[${index}]`, out); + } + return out; + } + if (node && typeof node === 'object') { + if (typeof node.string === 'string' && TEZOS_ADDRESS_PATTERN.test(node.string)) { + out.set(path, node.string); + } + if (typeof node.bytes === 'string') { + const address = decodeTezosAddressBytes(node.bytes); + if (address) { + out.set(path, address); + } + } + for (const [key, value] of Object.entries(node)) { + extractAddressBindings(value, `${path}.${key}`, out); + } + } + return out; +} + +function diffAddressBindings(expected, actual) { + const mismatches = []; + const paths = new Set([...expected.keys(), ...actual.keys()]); + for (const path of paths) { + const expectedAddress = expected.get(path); + const actualAddress = actual.get(path); + if (expectedAddress !== actualAddress) { + mismatches.push({ path, expected: expectedAddress, actual: actualAddress }); + } + } + return mismatches; +} + +// Pure guard used by createTezosClient/syncDeploymentOriginator/runDeployment to +// reject a manifest or config that was produced for a different chain than the one +// currently connected. Extracted as a standalone function (no network access) so it +// can be unit-tested directly instead of only indirectly via a live RPC connection. +function checkChainIdMatch(expectedChainId, actualChainId, context) { + if (expectedChainId && expectedChainId !== actualChainId) { + throw new Error( + `${context} was created for/configured with chain ${expectedChainId}, but the connected ` + + `RPC reports chain ${actualChainId}. Refusing to proceed.`, + ); + } +} + +async function enforceDeploymentPreflight(deployResultPath, networkProfile, chainId, preflight, oracleVerification) { + const isMainnet = networkProfile === 'mainnet' || chainId === 'NetXdQprcVkpaWU'; + if (!isMainnet) { + return; + } + const runPreflight = preflight || require('./mainnet_preflight.js').mainnetPreflight; + await runPreflight(deployResultPath); + const verifyOracle = oracleVerification || require('./verify_mainnet_oracle.js').verifyMainnetOracle; + await verifyOracle(deployResultPath); +} + +// Verify that a manifest entry still points at a live, matching contract before we +// trust it enough to skip re-deploying. This prevents silently reusing a stale or +// unrelated address just because a key happens to exist in the manifest (e.g. a +// manifest copied from another network, or an address that was never confirmed). +async function verifyExistingContract(tezos, address, expectedCode, expectedStorage, directoryName) { + let script; + try { + script = await fetchOnChainScript(tezos, address); + } catch (error) { + throw new Error( + `Manifest entry "${directoryName}" points to ${address}, but no contract could be found ` + + `there on the connected chain (${error.message}). Refusing to skip deployment; remove the ` + + `stale entry from the manifest or fix the address before re-running.`, + ); + } + + if (!michelineScriptEqual(script.code, expectedCode)) { + throw new Error( + `Manifest entry "${directoryName}" (${address}) does not match the compiled contract code ` + + `on the connected chain. Refusing to skip deployment; the on-chain contract may belong to a ` + + `different network/version than the one currently compiled.`, + ); + } + + const onChainStorage = await fetchOnChainStorage(tezos, address); + if (directoryName.endsWith('_IRM') && !micheline_equal(onChainStorage, expectedStorage)) { + throw new Error( + `Manifest entry "${directoryName}" (${address}) has matching code but its immutable IRM ` + + `parameters do not match the compiled initial storage. Refusing to skip deployment; ` + + `verify the base rate, multiplier, jump multiplier, kink, and scale.`, + ); + } + const expectedAddresses = extractAddressBindings(expectedStorage); + const actualAddresses = extractAddressBindings(onChainStorage); + const addressMismatches = diffAddressBindings(expectedAddresses, actualAddresses); + if (addressMismatches.length > 0) { + const details = addressMismatches.map(({ path, expected, actual }) => + `${path}: expected ${expected || '(none)'}, actual ${actual || '(none)'}`, + ); + throw new Error( + `Manifest entry "${directoryName}" (${address}) has matching code but its on-chain storage ` + + `has different role-aware address bindings (admin/oracle/underlying/IRM/etc.) than the ` + + `compiled initial storage. Mismatches: [${details.join('; ')}]. Refusing to skip deployment; ` + + `this looks like the same contract template wired to a different configuration.`, + ); + } + console.log(`[INFO] Verified ${directoryName} at ${address} matches compiled code and critical storage addresses on-chain`); +} + +async function runDeployment(compiledContractsPath, deployResultPath) { + const { tezos, publicKeyHash, chainId } = await createTezosClient(); + await enforceDeploymentPreflight( + deployResultPath, config.networkProfile, chainId, + ); + console.log(`[INFO] Deploying from ${publicKeyHash} to ${config.tezosNode} (${chainId})`); + + const directories = getDirectories(compiledContractsPath).sort(); + if (directories.length === 0) { + throw new Error(`No compiled contract directories found in ${compiledContractsPath}`); + } + const jsonDeployResult = readDeployResult(deployResultPath); + + // Bind the manifest to the chain it was created for. A manifest produced against one + // network (e.g. a stale Previewnet run) must never be silently reused to skip + // origination against a different chain (e.g. mainnet). + if (jsonDeployResult.chainId) { + try { + checkChainIdMatch(jsonDeployResult.chainId, chainId, `Deploy manifest ${deployResultPath}`); + } catch (error) { + throw new Error(`${error.message} Refusing to reuse this manifest; start a fresh manifest for this network instead.`); + } + } + jsonDeployResult.chainId = chainId; + jsonDeployResult.network = config.tezosNode; + writeDeployResult(deployResultPath, JSON.stringify(jsonDeployResult, null, ' ')); + + const knownKeys = new Set([...directories, 'chainId', 'network', 'OriginatorAddress']); + const staleKeys = Object.keys(jsonDeployResult).filter((key) => !knownKeys.has(key)); + if (staleKeys.length > 0) { + console.log( + `[WARN] Manifest ${deployResultPath} contains keys not produced by this compile batch: ` + + `[${staleKeys.join(', ')}]. These are not verified against the current compiled output and ` + + `may be stale (e.g. from an older/partial compile run); double-check them manually or start ` + + `from an empty manifest.`, + ); + } + for (const directoryName of directories) { - console.log(`~~ deploy ${directoryName}`); - directoryPath = path.join(compiledContractsPath, directoryName); - code = getMichelsonCode(directoryPath); - storage = getMichelsonStorage(directoryPath); + const directoryPath = path.join(compiledContractsPath, directoryName); + const code = getMichelsonCode(directoryPath); + const storage = getMichelsonStorage(directoryPath); + + const existingAddress = jsonDeployResult[directoryName]; + if (existingAddress) { + await verifyExistingContract(tezos, existingAddress, code, storage, directoryName); + console.log(`[INFO] Skipping ${directoryName}; already deployed and verified at ${existingAddress}`); + continue; + } - contractAddress = await deployMichelsonContract(code, storage); + console.log(`[INFO] Deploying ${directoryName}`); + const contractAddress = await deployMichelsonContract(tezos, code, storage, directoryName); jsonDeployResult[directoryName] = contractAddress; writeDeployResult(deployResultPath, JSON.stringify(jsonDeployResult, null, ' ')); - console.log('~~~~~~~~~~~SUCCESS~~~~~~~~~~~~~~'); + console.log(`[INFO] ${directoryName} deployed successfully`); } } +async function run() { + return runDeployment( + path.join(__dirname, '../../TezFinBuild/compiled_contracts'), + resolveDeployResultPath(), + ); +} + const runE2E = async () => { - console.log(`[INFO] Log level ${log.getLevel()}`); - compiledContractsPath = path.join( - __dirname, - "../../e2e/compiled_contracts" - ); - const directories = getDirectories(compiledContractsPath); - console.log(directories); - await initAccount(); - deployResultPath = path.join( - __dirname, - "../../e2e/deploy_result/deploy.json" + return runDeployment( + path.join(__dirname, '../../e2e/compiled_contracts'), + path.join(__dirname, '../../e2e/deploy_result/deploy.json'), ); - jsonDeployResult = readDeployResult(deployResultPath); - for (const directoryName of directories) { - console.log(`~~ deploy ${directoryName}`); - directoryPath = path.join(compiledContractsPath, directoryName); - code = getMichelsonCode(directoryPath); - storage = getMichelsonStorage(directoryPath); - - contractAddress = await deployMichelsonContract(code, storage); - - jsonDeployResult[directoryName] = contractAddress; - writeDeployResult( - deployResultPath, - JSON.stringify(jsonDeployResult, null, " ") - ); - console.log("~~~~~~~~~~~SUCCESS~~~~~~~~~~~~~~"); - } }; -module.exports = {runE2E, run} + +// Verify that the PriceOracle address recorded in the manifest actually exists on the +// connected chain before TezFinOracle (and therefore Comptroller) is compiled against +// it. This closes the gap where a wrong/unrelated address could be silently baked into +// TezFinOracle's storage just because a key happens to be present in the manifest. +async function verifyOracleAddress(deployResultPath) { + const { tezos, chainId } = await createTezosClient(); + const deployResult = readDeployResult(deployResultPath); + const address = deployResult.PriceOracle; + if (!address) { + throw new Error( + `No "PriceOracle" address found in ${deployResultPath}. For Previewnet, run ` + + `CompileTestData.py to deploy the mock oracle first. For mainnet, add the vetted ` + + `production oracle address to the manifest before compiling TezFinOracle.`, + ); + } + try { + await tezos.rpc.getScript(address); + } catch (error) { + throw new Error( + `PriceOracle address ${address} from ${deployResultPath} could not be found on chain ` + + `${chainId} (${error.message}). Refusing to compile TezFinOracle against a nonexistent ` + + `oracle; fix the manifest before continuing.`, + ); + } + console.log(`[INFO] Verified PriceOracle ${address} exists on chain ${chainId}`); +} + +module.exports = { + checkConnection, + runE2E, + run, + syncDeploymentOriginator, + verifyOracleAddress, + config, + createTezosClient, + resolveDeployResultPath, + // Exported for unit testing (pure/offline functions, no network access): + checkChainIdMatch, + canonicalizeMicheline, + micheline_equal, + michelineScriptEqual, + decodeTezosAddressBytes, + extractAddressBindings, + diffAddressBindings, + verifyExistingContract, + enforceDeploymentPreflight, +} diff --git a/deploy/deploy_script/verify_mainnet_oracle.js b/deploy/deploy_script/verify_mainnet_oracle.js new file mode 100644 index 00000000..80e99bfe --- /dev/null +++ b/deploy/deploy_script/verify_mainnet_oracle.js @@ -0,0 +1,89 @@ +const fs = require('fs'); + +const { config, resolveDeployResultPath } = require('./util.js'); + +const ASSETS = ['XTZUSDT', 'USDTUSDT', 'TZBTCUSDT']; +const DEFAULT_MAX_AGE_SECONDS = 300; + +async function rpcJson(rpc, pathname, options = {}) { + const response = await fetch(`${rpc.replace(/\/$/, '')}${pathname}`, options); + if (!response.ok) { + throw new Error(`RPC ${pathname} returned ${response.status}: ${await response.text()}`); + } + return response.json(); +} + +function parsePriceResult(asset, response, headTimestamp, maxAgeSeconds) { + const args = response?.data?.args; + const price = Number(args?.[0]?.int); + const rawTimestamp = Number(args?.[1]?.int); + if (!Number.isSafeInteger(price) || price <= 0) { + throw new Error(`${asset} returned an invalid or zero price: ${args?.[0]?.int}`); + } + if (!Number.isSafeInteger(rawTimestamp) || rawTimestamp <= 0) { + throw new Error(`${asset} returned an invalid timestamp: ${args?.[1]?.int}`); + } + + if (rawTimestamp > 100000000000) { + throw new Error(`${asset} timestamp must use Unix seconds, not milliseconds: ${rawTimestamp}`); + } + const timestamp = rawTimestamp; + const ageSeconds = headTimestamp - timestamp; + if (ageSeconds < 0) { + throw new Error( + `${asset} timestamp ${rawTimestamp} is ${Math.abs(ageSeconds)} seconds ahead of the mainnet head.`, + ); + } + if (ageSeconds > maxAgeSeconds) { + throw new Error(`${asset} price is stale (${ageSeconds}s old; maximum ${maxAgeSeconds}s).`); + } + return { asset, price, timestamp, rawTimestamp, ageSeconds }; +} + +async function verifyMainnetOracle(deployResultPath = resolveDeployResultPath()) { + const manifest = JSON.parse(fs.readFileSync(deployResultPath, 'utf8')); + const oracle = manifest.PriceOracle; + if (!oracle) { + throw new Error('Mainnet manifest is missing PriceOracle.'); + } + + const rpc = process.env.TEZOS_RPC || config.tezosNode; + const source = config.originator?.pkh; + const maxAgeSeconds = Number(process.env.ORACLE_MAX_AGE_SECONDS || DEFAULT_MAX_AGE_SECONDS); + if (!source || !Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) { + throw new Error('A public originator pkh and a positive integer ORACLE_MAX_AGE_SECONDS are required.'); + } + + const [chainId, header] = await Promise.all([ + rpcJson(rpc, '/chains/main/chain_id'), + rpcJson(rpc, '/chains/main/blocks/head/header'), + ]); + const headTimestamp = Math.floor(Date.parse(header.timestamp) / 1000); + for (const asset of ASSETS) { + const response = await rpcJson(rpc, '/chains/main/blocks/head/helpers/scripts/run_script_view', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + contract: oracle, + view: 'get_price_with_timestamp', + input: { string: asset }, + chain_id: chainId, + source, + payer: source, + gas: '1040000', + unparsing_mode: 'Readable', + }), + }); + const result = parsePriceResult(asset, response, headTimestamp, maxAgeSeconds); + console.log(`[INFO] ${asset}: price=${result.price}, timestamp=${result.timestamp}, age=${result.ageSeconds}s`); + } +} + +if (require.main === module) { + verifyMainnetOracle().catch((error) => { + console.error(`[ERROR] Mainnet oracle verification failed: ${error.message}`); + process.exitCode = 1; + }); +} + +module.exports = { parsePriceResult, verifyMainnetOracle }; \ No newline at end of file diff --git a/deploy/deploy_script/verify_oracle.js b/deploy/deploy_script/verify_oracle.js new file mode 100644 index 00000000..f0c90e26 --- /dev/null +++ b/deploy/deploy_script/verify_oracle.js @@ -0,0 +1,11 @@ +const { verifyOracleAddress, resolveDeployResultPath } = require('./util.js'); + +// Run this before compiling CompileTezFinOracle.py to make sure the PriceOracle +// address recorded in the manifest actually exists on the connected chain, instead of +// only checking that the manifest key is present. +const deployResultPath = resolveDeployResultPath(); + +verifyOracleAddress(deployResultPath).catch((error) => { + console.error(`[ERROR] PriceOracle verification failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/deploy/shell_scripts/deploy_all_contracts.sh b/deploy/shell_scripts/deploy_all_contracts.sh deleted file mode 100755 index 661a1ca0..00000000 --- a/deploy/shell_scripts/deploy_all_contracts.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately -# 1 - path to SmartPy.sh -# example: ./deploy/shell_scripts/deploy_all_contracts.sh ~/smartpy-cli/SmartPy.sh - -($1 compile ./deploy/compile_targets/CompileTestData.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu)\ -&& echo "CompileTestData.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileTezFinOracle.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu)\ -&& echo "CompileTezFinOracle.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileGovernance.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu)\ -&& echo "CompileGovernance.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileComptroller.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileComptroller.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileIRMs.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu)\ -&& echo "CompileIRMs.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileCUSDt.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCUSDt.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileCUSDtz.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCUSDtz.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileCXTZ.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCXTZ.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - -($1 compile ./deploy/compile_targets/CompileTzBTC.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileTzBTC.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js \ No newline at end of file diff --git a/deploy/shell_scripts/deploy_cfa12.sh b/deploy/shell_scripts/deploy_cfa12.sh deleted file mode 100755 index 5fc9ecae..00000000 --- a/deploy/shell_scripts/deploy_cfa12.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately -# 1 - path to SmartPy.sh -# example: ./deploy/shell_scripts/deploy_cfa12.sh ~/smartpy-cli/SmartPy.sh - - -($1 compile ./deploy/compile_targets/CompileCFA12_IRM.py ./TezFinBuild/compiled_contracts --purge --protocol granada)\ -&& echo "CompileCFA12_IRM.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - - -($1 compile ./deploy/compile_targets/CompileCFA12.py ./TezFinBuild/compiled_contracts --purge --protocol granada --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCFA12.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js diff --git a/deploy/shell_scripts/deploy_cfa2.sh b/deploy/shell_scripts/deploy_cfa2.sh deleted file mode 100755 index b0f09e34..00000000 --- a/deploy/shell_scripts/deploy_cfa2.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately -# 1 - path to SmartPy.sh -# example: ./deploy/shell_scripts/deploy_cfa2.sh ~/smartpy-cli/SmartPy.sh - - -($1 compile ./deploy/compile_targets/CompileCFA2_IRM.py ./TezFinBuild/compiled_contracts --purge --protocol granada)\ -&& echo "CompileCFA2_IRM.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js - - -($1 compile ./deploy/compile_targets/CompileCFA2.py ./TezFinBuild/compiled_contracts --purge --protocol granada --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCFA2.py was successfully compiled" || echo -node ./deploy/deploy_script/deploy.js diff --git a/deploy/shell_scripts/deploy_comptroller.sh b/deploy/shell_scripts/deploy_comptroller.sh index c6578ab0..82eb60ca 100644 --- a/deploy/shell_scripts/deploy_comptroller.sh +++ b/deploy/shell_scripts/deploy_comptroller.sh @@ -1,9 +1,10 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately +#!/usr/bin/env bash +set -euo pipefail # 1 - path to SmartPy.sh # example: ./deploy/shell_scripts/deploy_comptroller.sh ~/smartpy-cli/SmartPy.sh -($1 compile ./deploy/compile_targets/CompileComptroller.py ./TezFinBuild/compiled_contracts --purge --protocol granada --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileComptroller.py was successfully compiled" || echo +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" +node ./deploy/deploy_script/prepare.js +"$smartpy" compile ./deploy/compile_targets/CompileComptroller.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast node ./deploy/deploy_script/deploy.js diff --git a/deploy/shell_scripts/deploy_cxtz.sh b/deploy/shell_scripts/deploy_cxtz.sh index 7a922c85..b34444ad 100755 --- a/deploy/shell_scripts/deploy_cxtz.sh +++ b/deploy/shell_scripts/deploy_cxtz.sh @@ -1,14 +1,10 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately +#!/usr/bin/env bash +set -euo pipefail # 1 - path to SmartPy.sh # example: ./deploy/shell_scripts/deploy_cxtz.sh ~/smartpy-cli/SmartPy.sh - - -($1 compile ./deploy/compile_targets/CompileCXTZ_IRM.py ./TezFinBuild/compiled_contracts --purge --protocol granada)\ -&& echo "CompileCXTZ_IRM.py was successfully compiled" || echo +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" +node ./deploy/deploy_script/prepare.js +"$smartpy" compile ./deploy/compile_targets/CompileCXTZ_IRM.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu node ./deploy/deploy_script/deploy.js - - -($1 compile ./deploy/compile_targets/CompileCXTZ.py ./TezFinBuild/compiled_contracts --purge --protocol granada --erase-comments --erase-var-annots --initial-cast)\ -&& echo "CompileCXTZ.py was successfully compiled" || echo +"$smartpy" compile ./deploy/compile_targets/CompileCXTZ.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu --erase-comments --erase-var-annots --initial-cast node ./deploy/deploy_script/deploy.js diff --git a/deploy/shell_scripts/deploy_governance.sh b/deploy/shell_scripts/deploy_governance.sh index cd345596..cb713b48 100644 --- a/deploy/shell_scripts/deploy_governance.sh +++ b/deploy/shell_scripts/deploy_governance.sh @@ -1,8 +1,9 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately +#!/usr/bin/env bash +set -euo pipefail # 1 - path to SmartPy.sh # example: ./deploy/shell_scripts/deploy_governance.sh ~/smartpy-cli/SmartPy.sh -($1 compile ./deploy/compile_targets/CompileGovernance.py ./TezFinBuild/compiled_contracts --purge --protocol granada)\ -&& echo "CompileGovernance.py was successfully compiled" || echo +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" +node ./deploy/deploy_script/prepare.js +"$smartpy" compile ./deploy/compile_targets/CompileGovernance.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu node ./deploy/deploy_script/deploy.js diff --git a/deploy/shell_scripts/deploy_mainnet.sh b/deploy/shell_scripts/deploy_mainnet.sh new file mode 100755 index 00000000..6cf13cc3 --- /dev/null +++ b/deploy/shell_scripts/deploy_mainnet.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail +# Deploys the protocol to mainnet using only canonical production token/oracle +# addresses. This script never runs CompileTestData.py and will refuse to proceed +# unless: +# - config.json declares networkProfile "mainnet" and the connected RPC reports a +# known mainnet chain id (see assert_network.js), +# - the manifest already contains vetted PriceOracle/USDt/tzBTC addresses that match +# the allowlist in mainnet_preflight.js and exist on-chain, +# - MAINNET_DEPLOY_CONFIRM=yes is set after reviewing the printed deployment plan. +# +# Order matters: the plan/confirmation check (mainnet_preflight.js) runs BEFORE +# prepare.js, so the manifest is never touched (not even to set OriginatorAddress) if +# the operator declines to confirm. +# +# 1 - path to SmartPy.sh +# example: MAINNET_DEPLOY_CONFIRM=yes ./deploy/shell_scripts/deploy_mainnet.sh ~/smartpy-cli/SmartPy.sh + +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" + +# Belt-and-suspenders: even though util.js/Config.py already default to +# deploy.mainnet.json when config.json declares networkProfile "mainnet", export it +# explicitly here so every child process (node and non-node alike) agrees on the same +# manifest file without relying solely on that implicit default. +export DEPLOY_MANIFEST="${DEPLOY_MANIFEST:-TezFinBuild/deploy_result/deploy.mainnet.json}" + +node ./deploy/deploy_script/assert_network.js mainnet + +# Sanity check: every market compile target must reference its own asset-specific IRM +# config key (e.g. tzBTC -> CtzBTC_IRM), not another market's by mistake. +python3 ./deploy/compile_targets/tests/test_irm_wiring.py + +# Sanity check: shell-script compile-target references exist, CtzBTC_IRM reads its own +# config block, and Config.py/util.js agree on the default manifest path per profile. +python3 ./deploy/compile_targets/tests/test_deploy_pipeline_wiring.py + +node ./deploy/deploy_script/mainnet_preflight.js +node ./deploy/deploy_script/prepare.js + +compile_and_deploy() { + "$smartpy" compile "$1" ./TezFinBuild/compiled_contracts --purge --protocol kathmandu "${@:2}" + node ./deploy/deploy_script/deploy.js +} + +# Intentionally does NOT run CompileTestData.py: mainnet must never originate mock +# tokens or a mock PriceOracle. PriceOracle/USDt/tzBTC are required to already be +# present (and verified against the allowlist) in the manifest by mainnet_preflight.js +# above. +node ./deploy/deploy_script/verify_oracle.js +compile_and_deploy ./deploy/compile_targets/CompileTezFinOracle.py +compile_and_deploy ./deploy/compile_targets/CompileGovernance.py +compile_and_deploy ./deploy/compile_targets/CompileComptroller.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileIRMs.py +compile_and_deploy ./deploy/compile_targets/CompileCtzBTC_IRM.py +compile_and_deploy ./deploy/compile_targets/CompileCUSDt.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileCXTZ.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileTzBTC.py --erase-comments --erase-var-annots --initial-cast + +echo "" +echo "[INFO] Mainnet origination complete. Do NOT unpause markets or open the protocol to users" +echo "[INFO] until the admin handoff checklist in README.md ('Post-Deployment Admin Handoff') has" +echo "[INFO] been fully completed and verified on-chain." diff --git a/deploy/shell_scripts/deploy_previewnet.sh b/deploy/shell_scripts/deploy_previewnet.sh new file mode 100755 index 00000000..a6d78c36 --- /dev/null +++ b/deploy/shell_scripts/deploy_previewnet.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail +# Deploys the full protocol to Previewnet, including mock test tokens and a mock +# PriceOracle. This script must NEVER be used for a mainnet deployment; use +# deploy_mainnet.sh instead, which refuses to run CompileTestData.py. +# +# 1 - path to SmartPy.sh +# example: ./deploy/shell_scripts/deploy_previewnet.sh ~/smartpy-cli/SmartPy.sh + +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" + +# Guard against accidentally pointing this script at mainnet: config.json (or +# DEPLOY_MANIFEST) must not resolve to a mainnet chain id. +node ./deploy/deploy_script/assert_network.js previewnet + +# Sanity check: every market compile target must reference its own asset-specific IRM +# config key (e.g. tzBTC -> CtzBTC_IRM), not another market's by mistake. +python3 ./deploy/compile_targets/tests/test_irm_wiring.py + +# Sanity check: shell-script compile-target references exist, CtzBTC_IRM reads its own +# config block, and Config.py/util.js agree on the default manifest path per profile. +python3 ./deploy/compile_targets/tests/test_deploy_pipeline_wiring.py + +node ./deploy/deploy_script/prepare.js + +compile_and_deploy() { + "$smartpy" compile "$1" ./TezFinBuild/compiled_contracts --purge --protocol kathmandu "${@:2}" + node ./deploy/deploy_script/deploy.js +} + +# WARNING: CompileTestData.py compiles/deploys a mock PriceOracle plus fake tzBTC, +# USDtz, and USDt tokens (with an admin-mint entry point). This is Previewnet-only +# test tooling. Do NOT run this script against mainnet; a mainnet deployment must +# supply the vetted production oracle and canonical token addresses directly in the +# manifest instead of originating these mocks. See README.md "Deployment Manifest". +compile_and_deploy ./deploy/compile_targets/CompileTestData.py +node ./deploy/deploy_script/verify_oracle.js +compile_and_deploy ./deploy/compile_targets/CompileTezFinOracle.py +compile_and_deploy ./deploy/compile_targets/CompileGovernance.py +compile_and_deploy ./deploy/compile_targets/CompileComptroller.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileIRMs.py +compile_and_deploy ./deploy/compile_targets/CompileCtzBTC_IRM.py +compile_and_deploy ./deploy/compile_targets/CompileCUSDt.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileCXTZ.py --erase-comments --erase-var-annots --initial-cast +compile_and_deploy ./deploy/compile_targets/CompileTzBTC.py --erase-comments --erase-var-annots --initial-cast diff --git a/deploy/shell_scripts/deploy_test_data.sh b/deploy/shell_scripts/deploy_test_data.sh index d38076dd..78703fc5 100644 --- a/deploy/shell_scripts/deploy_test_data.sh +++ b/deploy/shell_scripts/deploy_test_data.sh @@ -1,8 +1,9 @@ -#!/bin/bash -set -e # Any subsequent(*) commands which fail will cause the shell script to exit immediately +#!/usr/bin/env bash +set -euo pipefail # 1 - path to SmartPy.sh # example: ./deploy/shell_scripts/deploy_test_data.sh ~/smartpy-cli/SmartPy.sh -($1 compile ./deploy/compile_targets/CompileTestData.py ./TezFinBuild/compiled_contracts --purge --protocol granada)\ -&& echo "CompileTestData.py was successfully compiled" || echo +smartpy="${1:?Usage: $0 /path/to/SmartPy.sh}" +node ./deploy/deploy_script/prepare.js +"$smartpy" compile ./deploy/compile_targets/CompileTestData.py ./TezFinBuild/compiled_contracts --purge --protocol kathmandu node ./deploy/deploy_script/deploy.js diff --git a/deploy/test_data/PriceOracle.py b/deploy/test_data/PriceOracle.py index 137033d9..c91ee4a4 100644 --- a/deploy/test_data/PriceOracle.py +++ b/deploy/test_data/PriceOracle.py @@ -10,7 +10,34 @@ def __init__(self, **extra_storage): def setPrice(self, params): sp.set_type(params, sp.TRecord(asset = sp.TString, price = sp.TNat)) self.data.prices[params.asset] = params.price - + + @sp.onchain_view() + def get_price_with_timestamp(self, requestedAsset): + sp.set_type(requestedAsset, sp.TString) + price = sp.local('price', sp.nat(0)) + sp.if self.data.prices.contains(requestedAsset): + price.value = self.data.prices[requestedAsset] + sp.result((price.value, sp.timestamp(0))) + + @sp.onchain_view() + def getPrice(self, requestedAsset): + sp.set_type(requestedAsset, sp.TString) + price = sp.local('price', sp.nat(0)) + sp.if self.data.prices.contains(requestedAsset): + price.value = self.data.prices[requestedAsset] + sp.result((sp.timestamp(0), price.value)) + + # OracleInterface declares getValidatedPrice; leaving it as `pass` makes SmartPy emit + # invalid Michelson (`prim: "ERROR"` / unknown type variable) that Previewnet rejects + # at origination. The mock does not enforce bounds — TezFinOracle does that for real. + @sp.onchain_view() + def getValidatedPrice(self, params): + sp.set_type(params, OracleInterface.TValidatedPriceRequest) + price = sp.local('price', sp.nat(0)) + sp.if self.data.prices.contains(params.requestedAsset): + price.value = self.data.prices[params.requestedAsset] + sp.result((sp.timestamp(0), price.value)) + @sp.entry_point def get(self, requestPair): sp.set_type(requestPair, OracleInterface.TGetPriceParam) diff --git a/docs/MainnetGovernancePayloads.json b/docs/MainnetGovernancePayloads.json new file mode 100644 index 00000000..ae3b92c4 --- /dev/null +++ b/docs/MainnetGovernancePayloads.json @@ -0,0 +1,228 @@ +{ + "status": "INITIAL_DRAFT", + "requiredInputs": { + "productionMultisig": "KT1Mc9rBcVmoLATY1FQDBvzSMHmUh6rgwjDR", + "maxPriceTimeDifferenceSeconds": 3600, + "priceBounds": { + "XTZ": { "minPrice": "TODO_VALUE", "maxPrice": "TODO_VALUE", "maxChangeBps": "TODO_VALUE" }, + "USDT": { "minPrice": "TODO_VALUE", "maxPrice": "TODO_VALUE", "maxChangeBps": "TODO_VALUE" }, + "TZBTC": { "minPrice": "TODO_VALUE", "maxPrice": "TODO_VALUE", "maxChangeBps": "TODO_VALUE" } + }, + "marketCaps": { + "XTZ": { "supplyCap": "TODO_VALUE", "borrowCap": "TODO_VALUE" }, + "USDT": { "supplyCap": "TODO_VALUE", "borrowCap": "TODO_VALUE" }, + "TZBTC": { "supplyCap": "TODO_VALUE", "borrowCap": "TODO_VALUE" } + }, + "collateralFactorMantissa": { + "XTZ": "500000000000000000", + "USDT": "500000000000000000", + "TZBTC": "500000000000000000" + }, + "activateAfterVerification": { + "XTZ": { "mint": false, "borrow": false, "redeem": false, "liquidate": false }, + "USDT": { "mint": false, "borrow": false, "redeem": false, "liquidate": false }, + "TZBTC": { "mint": false, "borrow": false, "redeem": false, "liquidate": false }, + "transfers": false + } + }, + "addressSources": { + "Governance": "TezFinBuild/deploy_result/deploy.mainnet.json#Governance", + "Comptroller": "TezFinBuild/deploy_result/deploy.mainnet.json#Comptroller", + "TezFinOracle": "TezFinBuild/deploy_result/deploy.mainnet.json#TezFinOracle", + "XTZ": "TezFinBuild/deploy_result/deploy.mainnet.json#CXTZ", + "USDT": "TezFinBuild/deploy_result/deploy.mainnet.json#CUSDt", + "TZBTC": "TezFinBuild/deploy_result/deploy.mainnet.json#CtzBTC" + }, + "phases": [ + { + "name": "deployer_stages_admin_handoff", + "operations": [ + { + "destination": "Governance", + "entrypoint": "setPendingGovernance", + "value": "productionMultisig" + }, + { + "destination": "TezFinOracle", + "entrypoint": "set_pending_admin", + "value": "productionMultisig" + } + ] + }, + { + "name": "multisig_accepts_admin_handoff", + "operations": [ + { + "destination": "Governance", + "entrypoint": "acceptGovernance", + "value": "Unit" + }, + { + "destination": "TezFinOracle", + "entrypoint": "accept_admin", + "value": "Unit" + } + ] + }, + { + "name": "multisig_configures_closed_markets", + "operations": [ + { + "destination": "Governance", + "entrypoint": "setPriceOracleAndTimeDiff", + "value": { + "comptroller": "Comptroller", + "priceOracle": "TezFinOracle", + "timeDiff": "maxPriceTimeDifferenceSeconds" + } + }, + { + "destination": "Governance", + "entrypoint": "supportMarket", + "value": { + "comptroller": "Comptroller", + "market": { "cToken": "XTZ", "name": "XTZ", "priceExp": 1000000000000 } + } + }, + { + "destination": "Governance", + "entrypoint": "supportMarket", + "value": { + "comptroller": "Comptroller", + "market": { "cToken": "USDT", "name": "USDT", "priceExp": 1000000000000 } + } + }, + { + "destination": "Governance", + "entrypoint": "supportMarket", + "value": { + "comptroller": "Comptroller", + "market": { "cToken": "TZBTC", "name": "TZBTC", "priceExp": 10000000000 } + } + }, + { + "destination": "Governance", + "entrypoint": "setPriceBounds", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "bounds": { + "cToken": "$market", + "minPrice": "priceBounds.$market.minPrice", + "maxPrice": "priceBounds.$market.maxPrice", + "maxChangeBps": "priceBounds.$market.maxChangeBps" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setMarketCaps", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "caps": { + "cToken": "$market", + "supplyCap": "marketCaps.$market.supplyCap", + "borrowCap": "marketCaps.$market.borrowCap" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setCollateralFactor", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "collateralFactor": { + "cToken": "$market", + "newCollateralFactor": "collateralFactorMantissa.$market" + } + } + } + ] + }, + { + "name": "multisig_verifies_closed_market_configuration", + "requiredStorageChecks": [ + "TezFinOracle.maxPriceAge[Comptroller] equals maxPriceTimeDifferenceSeconds", + "TezFinOracle.priceBounds[(Comptroller, $market)] equals priceBounds.$market for every market", + "Comptroller.markets[$market].supplyCap and borrowCap equal marketCaps.$market for every market", + "Comptroller.markets[$market].collateralFactor equals collateralFactorMantissa.$market for every market", + "Mint, borrow, redeem, and liquidation remain paused for every market", + "Transfers remain paused" + ], + "blockingConditions": [ + "No TODO_VALUE values remain", + "Every risk parameter has formal approval", + "Every required storage check has passed against the target deployment" + ] + }, + { + "name": "multisig_opens_approved_markets", + "preconditions": [ + "Admin handoff storage has been verified on-chain", + "The production oracle guard passes for XTZUSDT, USDTUSDT, and TZBTCUSDT", + "Exact-transfer review is complete for the canonical USDt and tzBTC contracts", + "Every collateral factor and pause state has formal risk approval" + ], + "operations": [ + { + "destination": "Governance", + "entrypoint": "setMintPaused", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "tokenState": { + "cToken": "$market", + "state": "!activateAfterVerification.$market.mint" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setBorrowPaused", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "tokenState": { + "cToken": "$market", + "state": "!activateAfterVerification.$market.borrow" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setRedeemPaused", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "tokenState": { + "cToken": "$market", + "state": "!activateAfterVerification.$market.redeem" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setLiquidatePaused", + "repeatFor": ["XTZ", "USDT", "TZBTC"], + "value": { + "comptroller": "Comptroller", + "tokenState": { + "cToken": "$market", + "state": "!activateAfterVerification.$market.liquidate" + } + } + }, + { + "destination": "Governance", + "entrypoint": "setTransferPaused", + "value": { + "comptroller": "Comptroller", + "state": "!activateAfterVerification.transfers" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/install-smartpy.sh b/tools/install-smartpy.sh new file mode 100755 index 00000000..5901fbbd --- /dev/null +++ b/tools/install-smartpy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly SMARTPY_VERSION="0.16.0" +readonly SMARTPY_ARCHIVE_URL="https://legacy.smartpy.io/cli/smartpy-cli.tar.gz" +readonly SMARTPY_ARCHIVE_SHA256="615e60659f3550d9d50623ab8e6390ba75a0e4bbd4ebb00bed1aab41743bc392" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly RUNTIME_DIR="${SCRIPT_DIR}/smartpy" + +prefix="${1:-${HOME}/smartpy-cli}" +archive="$(mktemp)" +trap 'rm -f "${archive}"' EXIT + +curl --fail --silent --show-error --location "${SMARTPY_ARCHIVE_URL}" --output "${archive}" +if command -v shasum >/dev/null; then + printf '%s %s\n' "${SMARTPY_ARCHIVE_SHA256}" "${archive}" | shasum -a 256 --check --status +else + printf '%s %s\n' "${SMARTPY_ARCHIVE_SHA256}" "${archive}" | sha256sum --check --status +fi + +rm -rf "${prefix}" +mkdir -p "${prefix}" +tar xzf "${archive}" -C "${prefix}" +rm -f "${prefix}/smartpyc" + +cp "${RUNTIME_DIR}/package.json" "${RUNTIME_DIR}/package-lock.json" "${prefix}/" +npm --prefix "${prefix}" ci --ignore-scripts --no-audit --no-fund + +actual_version="$("${prefix}/SmartPy.sh" --version)" +if [[ "${actual_version}" != "SmartPy Version: ${SMARTPY_VERSION}" ]]; then + printf 'Expected SmartPy %s, got: %s\n' "${SMARTPY_VERSION}" "${actual_version}" >&2 + exit 1 +fi + +printf 'Installed verified SmartPy %s at %s\n' "${SMARTPY_VERSION}" "${prefix}" diff --git a/tools/smartpy/package-lock.json b/tools/smartpy/package-lock.json new file mode 100644 index 00000000..a43eece8 --- /dev/null +++ b/tools/smartpy/package-lock.json @@ -0,0 +1,108 @@ +{ + "name": "tezfin-smartpy-runtime", + "version": "0.16.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tezfin-smartpy-runtime", + "version": "0.16.0", + "dependencies": { + "@smartpy/timelock": "0.0.9", + "bs58check": "4.0.0", + "js-sha3": "0.9.3", + "libsodium-wrappers-sumo": "0.8.4", + "tezos-bls12-381": "1.1.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@smartpy/hacl-wasm": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@smartpy/hacl-wasm/-/hacl-wasm-0.0.3.tgz", + "integrity": "sha512-qKDOmdxHYyvEYbhMRMrPeehggM9iYPIukpOQl5M0kdYVKY3pH8YftonYxRJx5sYZ+CFqjNKj2pCk31pzTZsA1Q==", + "license": "Apache-2.0" + }, + "node_modules/@smartpy/timelock": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@smartpy/timelock/-/timelock-0.0.9.tgz", + "integrity": "sha512-opX43ZPK7gXCkrcUV/efykHx1efsAgw5ItZQ6PwHBS+SVl25CYc6Bz/r3z/gPELgxNh9KKKadYxAEWNekWJxMQ==", + "license": "MIT", + "dependencies": { + "@smartpy/hacl-wasm": "^0.0.3" + } + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, + "node_modules/js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==", + "license": "MIT" + }, + "node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, + "node_modules/noble-bls12-381": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/noble-bls12-381/-/noble-bls12-381-0.11.1.tgz", + "integrity": "sha512-72jd9c1VwGQz4y/OKgdNNKDDGNDkAInka6iiq7hVHCiqPWH4DTlW8KwDfvpsVnbK8t+5Sg6mG7r0CRqiUWwP7g==", + "deprecated": "Switch to \"@noble/curves\" for security updates", + "license": "MIT" + }, + "node_modules/tezos-bls12-381": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tezos-bls12-381/-/tezos-bls12-381-1.1.1.tgz", + "integrity": "sha512-MFaxhyRON/T/SbQWZnFcTZo8kSxhdP0IY9zfeMxHWUT+StgoEzJltAEYxiiSlQQqyLJyd67YsCGmGBqW+4mJSA==", + "license": "MIT", + "dependencies": { + "noble-bls12-381": "0.11.1" + } + } + } +} diff --git a/tools/smartpy/package.json b/tools/smartpy/package.json new file mode 100644 index 00000000..c491385e --- /dev/null +++ b/tools/smartpy/package.json @@ -0,0 +1,12 @@ +{ + "name": "tezfin-smartpy-runtime", + "version": "0.16.0", + "private": true, + "dependencies": { + "@smartpy/timelock": "0.0.9", + "bs58check": "4.0.0", + "js-sha3": "0.9.3", + "libsodium-wrappers-sumo": "0.8.4", + "tezos-bls12-381": "1.1.1" + } +}