diff --git a/.github/workflows/protocol-contracts-confidential-wrapper-tests.yml b/.github/workflows/protocol-contracts-confidential-wrapper-tests.yml new file mode 100644 index 0000000000..5ba1e86e8c --- /dev/null +++ b/.github/workflows/protocol-contracts-confidential-wrapper-tests.yml @@ -0,0 +1,77 @@ +# Run tests +name: protocol-contracts-confidential-wrapper-tests + +on: + pull_request: + + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: ci-protocol-contracts-confidential-wrapper-tests-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + check-changes: + name: protocol-contracts-confidential-wrapper-tests/check-changes + permissions: + actions: 'read' # Required to read workflow run information + contents: 'read' # Required to checkout repository code + pull-requests: 'read' # Required to read pull request information + runs-on: ubuntu-latest + outputs: + changes-protocol-contracts-confidential-wrapper: ${{ steps.filter.outputs.protocol-contracts-confidential-wrapper }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: 'false' + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + protocol-contracts-confidential-wrapper: + - .github/workflows/protocol-contracts-confidential-wrapper-tests.yml + - protocol-contracts/staking/** + tests: + name: protocol-contracts-confidential-wrapper-tests/tests (bpr) + needs: check-changes + # if: ${{ needs.check-changes.outputs.changes-protocol-contracts-confidential-wrapper == 'true' }} + # TODO: Re-enable this once the tests are fixed + # See https://github.com/zama-ai/fhevm-internal/issues/796 + if: false + runs-on: ubuntu-latest + permissions: + contents: 'read' # Required to checkout repository code + steps: + - name: Checkout project + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: 'false' + + - name: Install dependencies + working-directory: protocol-contracts/confidential-wrapper + run: npm ci + + - name: Run lint + working-directory: protocol-contracts/confidential-wrapper + run: npm run lint + + - name: Run compile + working-directory: protocol-contracts/confidential-wrapper + run: npm run compile + + - name: Run build:ts + working-directory: protocol-contracts/confidential-wrapper + run: npm run build:ts + + - name: Run unit tests + working-directory: protocol-contracts/confidential-wrapper + run: make test + + - name: Run coverage + working-directory: protocol-contracts/confidential-wrapper + run: make run coverage \ No newline at end of file diff --git a/.github/workflows/protocol-contracts-staking-hardhat-tests.yml b/.github/workflows/protocol-contracts-staking-hardhat-tests.yml index dafdbcfd3d..fb3aefa85e 100644 --- a/.github/workflows/protocol-contracts-staking-hardhat-tests.yml +++ b/.github/workflows/protocol-contracts-staking-hardhat-tests.yml @@ -7,11 +7,12 @@ on: push: branches: - main + workflow_dispatch: permissions: {} concurrency: - group: ci-tests-${{ github.ref }} + group: ci-protocol-contracts-staking-tests-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: @@ -52,6 +53,14 @@ jobs: working-directory: protocol-contracts/staking run: npm ci + - name: Run lint + working-directory: protocol-contracts/staking + run: npm run lint + + - name: Run compile + working-directory: protocol-contracts/staking + run: npm run compile + - name: Run unit tests working-directory: protocol-contracts/staking run: make test diff --git a/protocol-contracts/confidential-wrapper/.eslintignore b/protocol-contracts/confidential-wrapper/.eslintignore new file mode 100644 index 0000000000..0df468962f --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.eslintignore @@ -0,0 +1,26 @@ +# fhevm +fhevmTemp +tmp + +# directories +.coverage_artifacts +.coverage_cache +.coverage_contracts +artifacts +build +cache +coverage +dist +node_modules +types + +# files +*.env +*.log +.DS_Store +.pnp.* +bun.lockb +coverage.json +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/protocol-contracts/confidential-wrapper/.eslintrc.yml b/protocol-contracts/confidential-wrapper/.eslintrc.yml new file mode 100644 index 0000000000..27672a35fb --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.eslintrc.yml @@ -0,0 +1,21 @@ +extends: + - "eslint:recommended" + - "plugin:@typescript-eslint/eslint-recommended" + - "plugin:@typescript-eslint/recommended" + - "prettier" +parser: "@typescript-eslint/parser" +parserOptions: + project: "tsconfig.json" +plugins: + - "@typescript-eslint" +root: true +rules: + "@typescript-eslint/no-floating-promises": + - error + - ignoreIIFE: true + ignoreVoid: true + "@typescript-eslint/no-inferrable-types": "off" + "@typescript-eslint/no-unused-vars": + - error + - argsIgnorePattern: "_" + varsIgnorePattern: "_" diff --git a/protocol-contracts/confidential-wrapper/.gitignore b/protocol-contracts/confidential-wrapper/.gitignore new file mode 100644 index 0000000000..e8bd7ce483 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.gitignore @@ -0,0 +1,25 @@ +# fhevm +fhevmTemp + +# directories +.coverage_artifacts +.coverage_cache +.coverage_contracts +artifacts +build +cache +coverage +dist +node_modules +types +deployments +tmp + +# files +*.env +*.log +.DS_Store +.pnp.* +coverage.json +pnpm-lock.yaml +yarn.lock diff --git a/protocol-contracts/confidential-wrapper/.prettierignore b/protocol-contracts/confidential-wrapper/.prettierignore new file mode 100644 index 0000000000..f88a655657 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.prettierignore @@ -0,0 +1,25 @@ +# fhevm +fhevmTemp + +# directories +.coverage_artifacts +.coverage_cache +.coverage_contracts +artifacts +build +cache +coverage +dist +node_modules +types + +# files +*.env +*.log +.DS_Store +.pnp.* +bun.lockb +coverage.json +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/protocol-contracts/confidential-wrapper/.prettierrc.yml b/protocol-contracts/confidential-wrapper/.prettierrc.yml new file mode 100644 index 0000000000..7008abd8c2 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.prettierrc.yml @@ -0,0 +1,21 @@ +plugins: + - "prettier-plugin-solidity" +printWidth: 120 +trailingComma: "all" + +overrides: + - files: "*.md" + options: + proseWrap: "always" + - files: "*.sol" + options: + compiler: "0.8.24" + parser: "solidity-parse" + tabWidth: 4 + - files: "*.ts" + options: + importOrder: ["", "^[./]"] + importOrderParserPlugins: ["typescript"] + importOrderSeparation: true + importOrderSortSpecifiers: true + parser: "typescript" diff --git a/protocol-contracts/confidential-wrapper/.solcover.js b/protocol-contracts/confidential-wrapper/.solcover.js new file mode 100644 index 0000000000..c61d16c777 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.solcover.js @@ -0,0 +1,4 @@ +module.exports = { + istanbulReporter: ["html", "lcov"], + skipFiles: ["test"], +}; diff --git a/protocol-contracts/confidential-wrapper/.solhint.json b/protocol-contracts/confidential-wrapper/.solhint.json new file mode 100644 index 0000000000..122f1f6915 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.solhint.json @@ -0,0 +1,19 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.4"], + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "named-parameters-mapping": "warn", + "no-console": "off", + "not-rely-on-time": "off", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ] + } +} diff --git a/protocol-contracts/confidential-wrapper/.solhintignore b/protocol-contracts/confidential-wrapper/.solhintignore new file mode 100644 index 0000000000..16dc0c0d7b --- /dev/null +++ b/protocol-contracts/confidential-wrapper/.solhintignore @@ -0,0 +1,3 @@ +# directories +**/artifacts +**/node_modules diff --git a/protocol-contracts/confidential-wrapper/DOCUMENTATION.org b/protocol-contracts/confidential-wrapper/DOCUMENTATION.org new file mode 100644 index 0000000000..0e9b380596 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/DOCUMENTATION.org @@ -0,0 +1,728 @@ +#+title: Zaiffer Smart Contracts - Auditor Documentation +#+AUTHOR: Development Team +#+DATE: 2025-09-04 +#+STARTUP: overview + +* Executive Summary + +This document provides comprehensive security and architectural documentation for the Zaiffer confidential token smart contract system built on Zama's FHEVM (Fully Homomorphic Encryption Virtual Machine). The system implements confidential fungible tokens where balances and transaction amounts are encrypted but can still be operated upon mathematically (ERC7984). + +** Key Security Features +- All balances and transfer amounts are encrypted using FHE (euint64) +- Regulatory compliance with sanctioned address checking +- Upgradeable confidential tokens with admin controls +- Modular fee management with encrypted and plaintext fee calculations +- Isolated fund storage per token type via dedicated wrapper contracts + +* Architecture Overview + +** Double Factory Pattern + +Due to Ethereum's 24KB contract size limit, the system employs a split factory architecture: + +1. **DeploymentCoordinator**: Orchestrates deployment and maintains compatibility interface +2. **WrapperFactory**: Specialized factory for deploying Wrapper contracts (~12-15KB) +3. **RegulatedERC7984UpgradeableFactory**: Specialized factory for confidential tokens (~15-18KB) + +This architecture solves the size constraint while maintaining clean separation of concerns. + +** Core Components + +1. **AdminProvider**: Centralized configuration hub +2. **FeeManager**: Handles all fee calculations (encrypted and plaintext) +3. **SanctionsList**: Manages sanctioned addresses +4. **Wrapper**: Per-token wrapper contract for fund custody +5. **RegulatedERC7984Upgradeable**: Confidential token with regulatory features + +* Fund Custody and Security Architecture + +** Wrapper Fund Isolation + +Each underlying token (ERC20 or ETH) has its own dedicated Wrapper contract that: +- Holds all original tokens for that specific type +- Has exclusive mint/burn permissions on its paired confidential token +- Cannot interact with other token pools + +** Security Implications of Fund Isolation + +| Risk Factor | Impact | Mitigation | +|----------------------------+---------------------------------------+--------------------------------------------------| +| Smart Contract Bug | LIMITED - affects only one token type | Fund isolation prevents cross-contamination | +| Upgrade Risk | LIMITED - independent upgrade cycles | Each token pair can be upgraded separately | +| Admin Key Compromise | HIGH - all tokens affected | Recommend multi-sig + timelock for AdminProvider | +| Individual Wrapper Exploit | MEDIUM - one token pool at risk | Does not affect other token types | + +**Critical Security Benefit**: Unlike monolithic architectures where a single vulnerability could affect all token pools simultaneously, this design limits the "blast radius" of any exploit to a single token type. + +* Permission Model and Access Control + +** AdminProvider (Central Authority) +- **Owner**: Has full control over system configuration +- **Powers**: + - Update FeeManager contract reference + - Update SanctionsList contract reference + - Update regulator address +- **Security Note**: This is the most privileged role - compromise affects entire system + +** FeeManager +- **Owner**: Zaiffer controlled wallet +- **Powers**: + - Set transfer fee basis points (0-10,000 = 0-100%) + - Set wrap/unwrap fee basis points + - Set deployment fees + - Set fee recipient address + - Enable/disable transfer fees + +** SanctionsList +Any contract that supports the [[https://go.chainalysis.com/chainalysis-oracle-docs.html][Chainalysis oracle]] ~isSanctioned~ interface. Initially, the oracle will be used. However, Zaiffer could later introduce a proxying contract to add rules. + +** DeploymentCoordinator +- **Owner**: Zaiffer controlled wallet +- **Powers**: + - Set admin provider + - Set wrapper factory + - Set confidential token factory + - Accept factory ownerships + +** RegulatedERC7984UpgradeableFactory +- **Owner**: Coordinator +- **Powers**: + - deploy token + +** WrapperFactory +- **Owner**: Coordinator +- **Powers**: + - deploy wrapper + +** Individual Wrapper Contracts +- **Owner**: DeploymentCoordinator +- **Permissions**: + - WRAPPER_ROLE on paired confidential token (exclusive mint/burn) + - References AdminProvider for fee and sanctions checking + +** RegulatedERC7984Upgradeable Tokens +- **DEFAULT_ADMIN_ROLE**: + - set to AdminProvider.owner() during deployment +- **WRAPPER_ROLE**: + - Granted to associated Wrapper contract only +- **Upgradeability**: + - Only DEFAULT_ADMIN_ROLE can authorize upgrades + +* Administrative Hub Pattern + +** Centralized Configuration Benefits + +The AdminProvider pattern provides: + +1. **Easy Updates**: Change FeeManager, SanctionsList or regulator address globally for all tokens +2. **Consistent Policies**: All tokens use same fee and sanctions configuration +3. **Flexibility**: Hot-swappable policy modules without touching individual contracts +4. **Gas Efficiency**: Single transaction updates policy for entire system + +** Configuration Flow + +#+begin_src +AdminProvider.setFeeManager(newFeeManager) + ↓ +All Wrappers automatically use new FeeManager + ↓ +All confidential tokens get new fee calculations +#+end_src + +** Risk Assessment + +- **Single Point of Configuration**: AdminProvider compromise affects all tokens +- **Upgrade Coordination**: Requires careful testing of new policy contracts +- **Gas Costs**: Administrative changes are one-time costs affecting all tokens + +* RegulatedERC7984Upgradeable Features + +** Upgradeability + +- **Pattern**: UUPS (Universal Upgradeable Proxy Standard) +- **Authorization**: Only DEFAULT_ADMIN_ROLE can authorize upgrades via `_authorizeUpgrade()` +- **Storage**: Uses namespaced storage slots to prevent collisions +- **Risk**: Upgrade could introduce vulnerabilities & centralization + +** Publicly Decryptable Total Supply + +#+begin_src solidity +function _setTotalSupply(euint64 totalSupply) internal virtual override { + ERC7984Storage storage $ = _getERC7984Storage(); + $._totalSupply = totalSupply; + FHE.allowThis($._totalSupply); + FHE.makePubliclyDecryptable($._totalSupply); // ← Public decryption +} +#+end_src + +**Rationale**: Since wrapping/unwrapping are the only mint/burn mechanisms and these operations are necessarily public (interacting with non-confidential tokens), total supply is inferable anyway. + +** Regulatory Features + +*** Regulator Access +- **Address**: Set via AdminProvider.regulator +- **Permissions**: Can decrypt all balances and transfer amounts +- **Implementation**: `FHE.allow(balance, regulator())` called on every balance update +- **Purpose**: Regulatory compliance and monitoring + +*** Sanctions Enforcement +- **Check Points**: All mint, burn, transfer, and approve operations +- **Implementation**: `_checkSanctions(address)` queries AdminProvider.sanctionsList +- **Effect**: Sanctioned addresses cannot participate in any token operations +- **Bypass**: No bypass mechanisms - enforcement is absolute + +*** ERC20-Compatible Allowances + +The system provides dual authorization models: + +1. **Operator Model (ERC7984)**: Time-bounded delegation via `setOperator()` +2. **Allowance Model (ERC20)**: Amount-based delegation via `confidentialApprove()` + +Both are implemented with encrypted amounts for confidentiality. + +* Fee Management Architecture + +** Fee Types and Formulas + +*** Transfer Fees (Confidential) +- **Type**: euint64 (encrypted) +- **Formula**: `fee = (amount * transferFeeBasisPoints) / 10,000` +- **Range**: 0-10,000 basis points (0-100%) +- **Implementation**: Uses FHE operations for confidentiality +- **Note**: Cannot be `view` function due to FHE state mutations + +#+begin_src solidity +function calculateTransferFee(euint64 amount) external returns (euint64) { + euint64 feeAmount = FHE.asEuint64(0); + if (transferFeeEnabled && transferFeeBasisPoints != 0) { + feeAmount = FHE.mul(amount, uint64(transferFeeBasisPoints)); + } + euint64 normalizedFeeAmount = FHE.div(feeAmount, 10_000); + // ... access control setup + return normalizedFeeAmount; +} +#+end_src + +*** Wrap Fees (Plaintext) +- **Type**: uint64 (plaintext) +- **Formula**: `fee = (amount * wrapFeeBasisPoints) / 10,000` +- **Range**: 0-10,000 basis points (0-100%) +- **Rationale**: Plaintext because wrapping necessarily reveals amounts + +*** Unwrap Fees (Plaintext) +- **Type**: uint64 (plaintext) +- **Formula**: `fee = (amount * unwrapFeeBasisPoints) / 10,000` +- **Range**: 0-10,000 basis points (0-100%) +- **Rationale**: Plaintext because unwrapping necessarily reveals amounts + +*** Deploy Fees (Plaintext) +- **Type**: uint64 (wei) +- **Purpose**: ETH fee for deploying new wrapper/token pairs +- **Payment**: Required in ETH via `msg.value` +- **Recipient**: FeeManager.feeRecipient + +** Fee Collection + +- **Transfer Fees**: Collected to feeRecipient during encrypted transfers +- **Wrap/Unwrap Fees**: Minted to feeRecipient during wrap/unwrap operations +- **Deploy Fees**: Direct ETH transfer to feeRecipient +- **Recipient**: Single address configurable via FeeManager + +** Fee Security Considerations + +1. **Encrypted Fee Leakage**: Transfer fees maintain confidentiality via FHE +2. **Fee Recipient Trust**: Single recipient address must be trusted/multisig +3. **Maximum Fee Protection**: All fees capped at 100% (10,000 basis points) +4. **Fee Bypass**: WRAPPER_ROLE exempt from transfer fees (prevents double-charging) + +* Critical Security Considerations + +** FHE Access Control (ACL) + +The system relies heavily on Zama's ACL for encrypted value access: + +- **allowThis()**: Grants contract access to encrypted values +- **allow()**: Grants specific address access +- **allowTransient()**: Temporary access for cross-contract calls +- **Critical**: Improper ACL management could leak encrypted data + +** Upgrade Risks + +1. **Storage Layout**: Upgrades must maintain storage compatibility +2. **Admin Role**: DEFAULT_ADMIN_ROLE has unchecked upgrade authority + +** Oracle Dependencies + +- **Decryption Oracle**: Required for unwrap operations (async) +- **Signature Verification**: `FHE.checkSignatures()` validates oracle responses +- **Failure Handling**: Failed unwraps refund encrypted tokens to user + +** Fund Recovery + +No built-in fund recovery mechanisms exist. Lost private keys or inaccessible encrypted balances cannot be recovered, which is by design for confidentiality. + +* Contract Interaction Diagrams + +** System Deployment Flow + +#+begin_src mermaid :file img/system-deployment-flow.png :width 2000 :exports results +sequenceDiagram + participant Admin as Admin + participant DC as DeploymentCoordinator + participant WF as WrapperFactory + participant TF as RegulatedERC7984UpgradeableFactory + participant AP as AdminProvider + participant FM as FeeManager + participant SL as SanctionsList + + Admin->>+FM: deploy FeeManager + Admin->>+SL: deploy SanctionsList + Admin->>+AP: deploy AdminProvider + Admin->>+WF: deploy WrapperFactory + Admin->>+TF: deploy RegulatedERC7984UpgradeableFactory + Admin->>+DC: deploy DeploymentCoordinator + + Admin->>WF: transferOwnership(DC) + Admin->>TF: transferOwnership(DC) + Admin->>DC: acceptOwnership(WF) + Admin->>DC: acceptOwnership(TF) +#+end_src + +#+RESULTS: +[[file:img/system-deployment-flow.png]] + +** New Token Deployment Flow + +Deploy Eth wrapper. For ERC20 wrappers, replace Eth transfer by a transfer from. + +#+begin_src mermaid :file img/token-deployment-flow.png :width 2000 :exports results +sequenceDiagram + participant User as User + participant DC as DeploymentCoordinator + participant WF as WrapperFactory + participant TF as TokenFactory + participant W as Wrapper + participant CT as ConfidentialToken + participant FM as FeeManager + + User->>+DC: deployNewToken(underlyingToken) {value: deployFee} + + DC->>FM: getDeployFee() + FM-->>DC: fee amount + + alt Insufficient Fee + DC-->>User: revert InsufficientFee + end + + DC->>+WF: deployWrapper(underlyingToken, adminProvider) + WF->>+W: deploy new Wrapper + W-->>WF: wrapper address + WF-->>DC: wrapper address + + DC->>+TF: deployConfidentialToken(name, symbol, adminProvider) + TF->>+CT: deploy new RegulatedERC7984Upgradeable + CT-->>TF: token address + TF-->>DC: token address + + DC->>CT: grantRole(WRAPPER_ROLE, W) + + DC->>FM: transferDeployFee() + + DC-->>User: (wrapper, confidentialToken) +#+end_src + +#+RESULTS: +[[file:img/token-deployment-flow.png]] + +** Token Wrapping Flow + +#+begin_src mermaid :file img/token-wrapping-flow.png :width 2000 :exports results +sequenceDiagram + participant User as User + participant ERC20 as UnderlyingToken + participant W as Wrapper + participant CT as ConfidentialToken + participant FM as FeeManager + participant SL as SanctionsList + participant AP as AdminProvider + + User->>+W: wrap(amount, userAddress) + + W->>AP: sanctionsList() + AP-->>W: SL address + W->>SL: isSanctioned(User) + SL-->>W: false + + alt User is Sanctioned + W-->>User: revert SanctionedAddress + end + + W->>AP: feeManager() + AP-->>W: FM address + W->>FM: calculateWrapFee(amount) + FM-->>W: fee + + W->>+ERC20: transferFrom(User, W, amount) + ERC20-->>W: success + + Note over W: mintAmount = amount - fee + + W->>+CT: mint(User, encryptedMintAmount, userPublicKey) + CT-->>W: success + + alt Fee > 0 + W->>FM: feeRecipient() + FM-->>W: recipient + W->>CT: mint(recipient, encryptedFee, recipientPublicKey) + end + + W-->>User: success +#+end_src + +#+RESULTS: +[[file:img/token-wrapping-flow.png]] + +** Token Unwrapping Flow (via transferAndCall) + +The unwrapping process now uses the transferAndCall pattern instead of separate approve/transferFrom calls. Users call `confidentialTransferAndCall` on the confidential token, which triggers the unwrapping process: + +#+begin_src mermaid :file img/token-unwrapping-flow.png :width 2000 :exports results +sequenceDiagram + participant User as User + participant CT as ConfidentialToken + participant W as Wrapper + participant FHE as FHE Oracle + participant ERC20 as UnderlyingToken + participant FM as FeeManager + participant AP as AdminProvider + participant SL as SanctionsList + participant Receiver as IWrapperReceiver + + User->>+CT: confidentialTransferAndCall(wrapper, encryptedAmount, callbackData) + Note over User,CT: callbackData = abi.encode(to, unwrapCallbackData) + + CT->>+W: onConfidentialTransferReceived(operator, from, amount, data) + + W->>AP: sanctionsList() + AP-->>W: SL address + W->>SL: isSanctioned(User) + SL-->>W: false + + W->>AP: feeManager() + AP-->>W: FM address + W->>FM: getUnwrapFee(encryptedAmount) + FM-->>W: encryptedFee + + Note over W: expectedBurnAmount = encryptedAmount - encryptedFee + + W->>CT: burn(expectedBurnAmount, from) + CT-->>W: actualBurnAmount + + W->>CT: confidentialTransfer(feeRecipient, expectedFee) + CT-->>W: actualFeeAmount + + W->>+FHE: requestDecryption([expectedBurnAmount, actualBurnAmount, expectedFee, actualFeeAmount, success]) + FHE-->>W: requestId + + Note over FHE: Async decryption process + + FHE->>+W: finalizeUnwrap(requestId, cleartexts, proof) + + alt Decryption Success && Amounts Match + W->>+ERC20: transfer(to, actualBurnAmount * rate) + ERC20-->>W: success + + alt Receiver is Contract + W->>+Receiver: onUnwrapFinalizedReceived(sender, amount, callbackData) + end + else Failure or Mismatch + Note over W: Refund tokens to user + W->>CT: mint(to, actualBurnAmount) + W->>CT: confidentialTransferFrom(feeRecipient, to, actualFeeAmount) + end + + W-->>User: UnwrappedFinalized event +#+end_src + +#+RESULTS: +[[file:img/token-unwrapping-flow.png]] + +** Confidential Transfer Flow + +#+begin_src mermaid :file img/confidential-transfer-flow.png :width 2000 :exports results +sequenceDiagram + participant Alice as Alice + participant Bob as Bob + participant CT as ConfidentialToken + participant FM as FeeManager + participant AP as AdminProvider + participant SL as SanctionsList + participant Reg as Regulator + + Alice->>+CT: confidentialTransfer(Bob, encryptedAmount, aliceSignature) + + CT->>AP: sanctionsList() + AP-->>CT: SL address + CT->>SL: isSanctioned(Alice) + SL-->>CT: false + CT->>SL: isSanctioned(Bob) + SL-->>CT: false + + alt Either party is sanctioned + CT-->>Alice: revert SanctionedAddress + end + + CT->>AP: feeManager() + AP-->>CT: FM address + CT->>FM: calculateTransferFee(encryptedAmount) + FM-->>CT: encryptedFee + + Note over CT: Verify Alice has sufficient encrypted balance + Note over CT: encryptedTransferAmount = encryptedAmount - encryptedFee + + CT->>CT: _updateBalance(Alice, -encryptedAmount) + CT->>CT: _updateBalance(Bob, +encryptedTransferAmount) + + alt Fee > 0 + CT->>FM: feeRecipient() + FM-->>CT: recipient + CT->>CT: _updateBalance(recipient, +encryptedFee) + end + + Note over CT: Grant regulator access to all encrypted values + CT->>AP: regulator() + AP-->>CT: Reg address + CT->>+FHE: allow(encryptedAmount, Reg) + CT->>FHE: allow(aliceNewBalance, Reg) + CT->>FHE: allow(bobNewBalance, Reg) + + CT-->>Alice: Transfer event (with encrypted placeholders) +#+end_src + +#+RESULTS: +[[file:img/confidential-transfer-flow.png]] + +** Admin Configuration Flow + +#+begin_src mermaid :file img/admin-configuration-flow.png :width 2000 :exports results +sequenceDiagram + participant Admin as Admin + participant AP as AdminProvider + participant FM as FeeManager + participant SL as SanctionsList + participant W1 as Wrapper1 + participant W2 as Wrapper2 + participant Wn as WrapperN + + Admin->>+AP: setFeeManager(newFeeManager) + AP-->>Admin: success + + Note over W1,Wn: All Wrappers automatically reference new FeeManager + + User->>W1: wrap(amount) + W1->>AP: feeManager() + AP-->>W1: newFeeManager address + W1->>+NewFeeManager: calculateWrapFee(amount) + + Admin->>+AP: setSanctionsList(newSanctionsList) + AP-->>Admin: success + + Note over W1,Wn: All operations now use new SanctionsList + + Admin->>+AP: setRegulator(newRegulator) + AP-->>Admin: success + + Note over CT: All future balance updates grant access to newRegulator + + Admin->>+FM: setTransferFeeBasisPoints(500) // 5% + FM-->>Admin: success + + Admin->>+FM: setFeeRecipient(newRecipient) + FM-->>Admin: success +#+end_src + +#+RESULTS: +[[file:img/admin-configuration-flow.png]] + +* Batch Transfer System (ERC7984TransferBatcher) + +The system includes a batch transfer contract that allows users to perform multiple confidential transfers in a single transaction, with retry functionality for failed transfers. + +** Architecture + +- **Contract**: `contracts/batcher/ERC7984TransferBatcher.sol` +- **Purpose**: Efficient batch processing of confidential transfers with fee collection +- **Features**: Retry mechanism for failed transactions, fee collection, sender tracking + +** Batch Transfer Flow + +#+begin_src mermaid :file img/batch-transfer-flow.png :width 2000 :exports results +sequenceDiagram + participant User as User + participant Batcher as ERC7984TransferBatcher + participant CT as ConfidentialToken + participant AP as AdminProvider + participant FM as FeeManager + + User->>+Batcher: confidentialBatchTransfer(cToken, transfers[]) {value: batchFee} + + Batcher->>AP: feeManager() + AP-->>Batcher: FM address + Batcher->>FM: getBatchTransferFee() + FM-->>Batcher: requiredFee + + alt Insufficient Fee + Batcher-->>User: revert InsufficientFee + end + + Batcher->>FM: getFeeRecipient() + FM-->>Batcher: feeRecipient + Batcher->>feeRecipient: transfer batch fee + + Note over Batcher: startTxId = cToken.nextTxId() + + loop For each transfer in transfers[] + alt Retry Transfer + Batcher->>Batcher: verify original sender == msg.sender + end + + Note over Batcher: Decrypt external encrypted amount + Batcher->>+CT: confidentialTransferFrom(sender, to, amount) + CT-->>Batcher: success + + Note over Batcher: Record txId -> sender mapping + + alt Retry Transfer + Batcher-->>User: RetryTransfer event + end + end + + Note over Batcher: endTxId = cToken.nextTxId() - 1 + Batcher-->>User: BatchTransfer event (startTxId, endTxId) +#+end_src + +#+RESULTS: +[[file:img/batch-transfer-flow.png]] + +** Key Features + +- **Fee Management**: Fixed ETH fee for batch operations, paid upfront +- **Retry Mechanism**: Users can retry failed transfers by referencing original txId +- **Sender Tracking**: Maps transaction IDs to original senders for retry validation +- **External Encryption**: Supports encrypted inputs with proofs for confidentiality +- **Event Logging**: Comprehensive events for batch operations and retries + +** Security Considerations + +- **Retry Authorization**: Only original sender can retry their failed transfers +- **Fee Collection**: Batch fees collected upfront prevent partial execution without payment +- **Input Validation**: External encrypted amounts validated with zero-knowledge proofs + +* Swap System (SwapV0) + +The swap system enables confidential token holders to swap their tokens through external DEXs (like Uniswap) while maintaining confidentiality during the process. + +** Architecture + +- **Contract**: `contracts/swap/swap_v0.sol` +- **Pattern**: Receiver pattern with transferAndCall integration +- **DEX Integration**: Uniswap V2 Router support with extensible interface +- **Failure Handling**: Automatic rewrapping on swap failure + +** Swap Flow + +#+begin_src mermaid :file img/swap-flow.png :width 2000 :exports results +sequenceDiagram + participant User as User + participant cTokenA as ConfidentialTokenA + participant SwapV0 as SwapV0 + participant WrapperA as WrapperA + participant TokenA as UnderlyingTokenA + participant Uniswap as UniswapV2Router + participant TokenB as UnderlyingTokenB + participant WrapperB as WrapperB + participant cTokenB as ConfidentialTokenB + participant FHE as FHE Oracle + + User->>+cTokenA: confidentialTransferAndCall(SwapV0, amount, swapData) + Note over User,cTokenA: swapData = abi.encode(router, amountOutMin, path, deadline, to) + + cTokenA->>+SwapV0: onConfidentialTransferReceived(operator, from, amount, data) + + SwapV0->>SwapV0: decode swap parameters + SwapV0->>+cTokenA: get nextTxId() for tracking + + Note over SwapV0: Prepare unwrap callback data + SwapV0->>+cTokenA: confidentialTransferAndCall(WrapperA, amount, callbackData) + + cTokenA->>+WrapperA: onConfidentialTransferReceived(SwapV0, from, amount, data) + + Note over WrapperA: Calculate fees and burn tokens + WrapperA->>+FHE: requestDecryption for unwrap amounts + FHE-->>WrapperA: requestId + + Note over FHE: Async decryption (1 block latency) + + FHE->>+WrapperA: finalizeUnwrap(requestId, cleartexts, proof) + + WrapperA->>+TokenA: transfer(SwapV0, unwrapAmount) + TokenA-->>WrapperA: success + + WrapperA->>+SwapV0: onUnwrapFinalizedReceived(operator, amount, callbackData) + + SwapV0->>+TokenA: approve(router, amountIn) + TokenA-->>SwapV0: success + + SwapV0->>+Uniswap: swapExactTokensForTokens(amountIn, amountOutMin, path, SwapV0, deadline) + + alt Swap Success + Uniswap-->>SwapV0: amounts[] (output amounts) + + SwapV0->>+TokenB: approve(WrapperB, amountOut) + TokenB-->>SwapV0: success + + SwapV0->>+WrapperB: wrap(amountOut, to) + WrapperB->>+cTokenB: mint(to, encryptedAmountOut) + + SwapV0-->>User: Swap event (success=true) + else Swap Failure + SwapV0->>+TokenA: approve(WrapperA, amountIn) + TokenA-->>SwapV0: success + + SwapV0->>+WrapperA: wrap(amountIn, to) (rewrap original tokens) + WrapperA->>+cTokenA: mint(to, encryptedAmountIn) + + SwapV0-->>User: Swap event (success=false) + end +#+end_src + +#+RESULTS: +[[file:img/swap-flow.png]] + +** Key Features + +- **Confidential Input**: Users send encrypted amounts that remain private during swap initiation +- **DEX Agnostic**: Supports any Uniswap V2-compatible router +- **Failure Recovery**: Failed swaps automatically rewrap original tokens back to user +- **Transaction Tracking**: Links unwrap and wrap operations for audit trails +- **Receiver Integration**: Uses IWrapperReceiver interface for seamless callbacks + +** Security Considerations + +- **MEV Protection**: Confidential amounts prevent sandwich attacks during swap setup +- **Slippage Protection**: Standard Uniswap slippage parameters (amountOutMin) +- **Deadline Protection**: Time-bound swaps prevent stale transactions +- **Atomic Recovery**: Failed swaps automatically return funds without manual intervention +- **Access Control**: Only confidential tokens can trigger swap operations + +** Gas and Latency + +- **Two-Block Process**: Unwrapping requires 1 block for FHE decryption, then swap execution +- **Gas Efficiency**: Single user transaction triggers entire swap flow +- **Event Tracking**: Comprehensive events link all transaction stages + +* Conclusion + +The Zaiffer system represents a sophisticated implementation of confidential tokens with regulatory compliance. The split factory architecture successfully addresses contract size limits while the AdminProvider pattern provides operational flexibility. Key security relies on proper FHE access control and careful upgrade management. + +The addition of batch transfers and swap functionality extends the system's capabilities while maintaining confidentiality and security. The transferAndCall pattern eliminates the need for separate approval transactions, improving user experience and gas efficiency. diff --git a/protocol-contracts/confidential-wrapper/LICENSE b/protocol-contracts/confidential-wrapper/LICENSE new file mode 100644 index 0000000000..9a72326eaa --- /dev/null +++ b/protocol-contracts/confidential-wrapper/LICENSE @@ -0,0 +1,33 @@ +BSD 3-Clause Clear License + +Copyright © 2025 ZAMA. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +3. Neither the name of ZAMA nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE*. +THIS SOFTWARE IS PROVIDED BY THE ZAMA AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +ZAMA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*In addition to the rights carried by this license, ZAMA grants to the user a non-exclusive, +free and non-commercial license on all patents filed in its name relating to the open-source +code (the "Patents") for the sole purpose of evaluation, development, research, prototyping +and experimentation. \ No newline at end of file diff --git a/protocol-contracts/confidential-wrapper/README.md b/protocol-contracts/confidential-wrapper/README.md new file mode 100644 index 0000000000..0ea93c304d --- /dev/null +++ b/protocol-contracts/confidential-wrapper/README.md @@ -0,0 +1,15 @@ +# FHEVM Hardhat Template + +A FHEVM Hardhat-based template for developing Solidity smart contracts. + +# Quick Start + +- [FHEVM Hardhat Quick Start Tutorial](https://docs.zama.ai/protocol/solidity-guides/getting-started/quick-start-tutorial) + +# Documentation + +- [The FHEVM documentation](https://docs.zama.ai/fhevm) +- [How to set up a FHEVM Hardhat development environment](https://docs.zama.ai/protocol/solidity-guides/getting-started/setup) +- [Run the FHEVM Hardhat Template Tests](https://docs.zama.ai/protocol/solidity-guides/development-guide/hardhat/run_test) +- [Write FHEVM Tests using Hardhat](https://docs.zama.ai/protocol/solidity-guides/development-guide/hardhat/write_test) +- [FHEVM Hardhart Plugin](https://docs.zama.ai/protocol/solidity-guides/development-guide/hardhat) diff --git a/protocol-contracts/confidential-wrapper/contracts/admin/AdminProvider.sol b/protocol-contracts/confidential-wrapper/contracts/admin/AdminProvider.sol new file mode 100644 index 0000000000..86a99e99b0 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/admin/AdminProvider.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {SanctionsList} from "./SanctionsList.sol"; +import {FeeManager} from "./FeeManager.sol"; + +/// @title IAdminProvider +/// @notice Interface for AdminProvider configuration management +interface IAdminProvider { + function sanctionsList() external view returns (SanctionsList); + function feeManager() external view returns (FeeManager); + function setSanctionsList(SanctionsList sanctionsList_) external; + function setFeeManager(FeeManager feeManager_) external; + + error ZeroAddressFeeManager(); + error ZeroAddressSanctionsList(); + error ZeroAddressRegulator(); + + event SanctionsListUpdated(address indexed oldSanctionsList, address indexed newSanctionsList); + event FeeManagerUpdated(address indexed oldFeeManager, address indexed newFeeManager); + event RegulatorUpdated(address indexed oldRegulator, address indexed newRegulator); +} + +/// @title AdminProvider +/// @notice Centralized configuration provider for confidential token system +/// @dev Provides shared access to sanctions list, fee manager, and regulator address +/// @dev Uses Ownable2Step for secure ownership transfers +/// @dev All setter functions are restricted to owner and validate against zero addresses +/// @custom:security-contact contact@zaiffer.org +contract AdminProvider is IAdminProvider, Ownable2Step { + /// @dev Current sanctions list contract + SanctionsList public sanctionsList; + + /// @dev Current fee manager contract + FeeManager public feeManager; + + /// @dev Address authorized to decrypt user handles + address public regulator; + + /// @notice Constructs AdminProvider with initial configuration + /// @param feeManager_ Initial fee manager contract address + /// @param sanctionsList_ Initial sanctions list contract address + /// @param regulator_ Initial regulator address for regulatory actions + /// @dev Reverts if any address is zero + /// @dev Sets msg.sender as initial owner via Ownable2Step + constructor(FeeManager feeManager_, SanctionsList sanctionsList_, address regulator_) Ownable(msg.sender) { + require(address(feeManager_) != address(0), ZeroAddressFeeManager()); + require(address(sanctionsList_) != address(0), ZeroAddressSanctionsList()); + require(regulator_ != address(0), ZeroAddressRegulator()); + feeManager = feeManager_; + sanctionsList = sanctionsList_; + regulator = regulator_; + } + + /// @notice Updates the sanctions list contract + /// @param sanctionsList_ New sanctions list contract address + /// @dev Only callable by owner + /// @dev Reverts if sanctionsList_ is zero address + /// @dev Emits SanctionsListUpdated event with old and new addresses + function setSanctionsList(SanctionsList sanctionsList_) external onlyOwner { + require(address(sanctionsList_) != address(0), ZeroAddressSanctionsList()); + address oldSanctionsList = address(sanctionsList); + sanctionsList = sanctionsList_; + emit SanctionsListUpdated(oldSanctionsList, address(sanctionsList_)); + } + + /// @notice Updates the fee manager contract + /// @param feeManager_ New fee manager contract address + /// @dev Only callable by owner + /// @dev Reverts if feeManager_ is zero address + /// @dev Emits FeeManagerUpdated event with old and new addresses + function setFeeManager(FeeManager feeManager_) external onlyOwner { + require(address(feeManager_) != address(0), ZeroAddressFeeManager()); + address oldFeeManager = address(feeManager); + feeManager = feeManager_; + emit FeeManagerUpdated(oldFeeManager, address(feeManager_)); + } + + /// @notice Updates the regulator address + /// @param regulator_ New regulator address + /// @dev Only callable by owner + /// @dev Reverts if regulator_ is zero address + /// @dev Emits RegulatorUpdated event with old and new addresses + function setRegulator(address regulator_) external onlyOwner { + require(regulator_ != address(0), ZeroAddressRegulator()); + address oldRegulator = regulator; + regulator = regulator_; + emit RegulatorUpdated(oldRegulator, regulator_); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/admin/FeeManager.sol b/protocol-contracts/confidential-wrapper/contracts/admin/FeeManager.sol new file mode 100644 index 0000000000..3ba42c39c3 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/admin/FeeManager.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; + +/// @title IFeeManager +/// @notice Interface for fee management in the confidential token system +interface IFeeManager { + function getWrapFee(uint256 amount, address wrapFrom, address wrapTo) external view returns (uint256); + function getUnwrapFee(uint64 amount, address unwrapFrom, address unwrapTo) external view returns (uint64); + function getDeployFee(address deployer) external view returns (uint64); + function getBatchTransferFee() external view returns (uint64); + function getFeeRecipient() external view returns (address); + function setWrapFeeBasisPoints(uint64 feeBasisPoints) external; + function setUnwrapFeeBasisPoints(uint64 feeBasisPoints) external; + function setSwapperWrapFeeBasisPoints(uint64 feeBasisPoints) external; + function setSwapperUnwrapFeeBasisPoints(uint64 feeBasisPoints) external; + function setDeployFee(uint64 deployFee) external; + function setBatchTransferFee(uint64 batchTransferFee) external; + function setFeeRecipient(address recipient) external; + function setSwapperFeeWaiverActive(bool active) external; + + event WrapFeeBasisPointsUpdated(uint64 oldFeeBasisPoints, uint64 newFeeBasisPoints); + event UnwrapFeeBasisPointsUpdated(uint64 oldFeeBasisPoints, uint64 newFeeBasisPoints); + event SwapperWrapFeeBasisPointsUpdated(uint64 oldFeeBasisPoints, uint64 newFeeBasisPoints); + event SwapperUnwrapFeeBasisPointsUpdated(uint64 oldFeeBasisPoints, uint64 newFeeBasisPoints); + event DeployFeeUpdated(uint64 oldDeployFee, uint64 newDeployFee); + event BatchTransferFeeUpdated(uint64 oldBatchTransferFee, uint64 newBatchTransferFee); + event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient); + event SwapperFeeWaiverUpdated(bool active); +} + +/// @title FeeManager +/// @notice Manages all fee configuration for the confidential token system +/// @dev Uses AccessControl with FEE_MANAGER_ROLE for fee parameter updates +/// @dev Supports both basis point fees (wrap/unwrap) and fixed fees (deploy/batch transfer) +/// @dev Fee ranges: +/// - Wrap/Unwrap: 0-10,000 basis points (0%-100%) +/// - Deploy: 0-type(uint64).max wei +/// - Batch Transfer: 0-type(uint64).max wei +/// @dev Special feature: SWAPPER_ROLE fee waiver for authorized swapper contracts +/// @custom:security-contact contact@zaiffer.org +contract FeeManager is IFeeManager, AccessControl, ZamaEthereumConfig { + /// @dev Role identifier for swapper contracts eligible for fee waivers + bytes32 public constant SWAPPER_ROLE = keccak256("SWAPPER_ROLE"); + + /// @dev Role identifier for accounts authorized to modify fee parameters + bytes32 public constant FEE_MANAGER_ROLE = keccak256("FEE_MANAGER_ROLE"); + + /// @dev Maximum allowed basis points (10,000 = 100%) + uint64 public constant MAX_BASIS_POINTS = 10_000; + + /// @dev Wrap fee in basis points (1 = 0.01%, 100 = 1%, 10000 = 100%) + uint64 public wrapFeeBasisPoints; + + /// @dev Unwrap fee in basis points (1 = 0.01%, 100 = 1%, 10000 = 100%) + uint64 public unwrapFeeBasisPoints; + + /// @dev Wrap fee for swappers in basis points when swapperFeeWaiverActive is true + uint64 public swapperWrapFeeBasisPoints; + + /// @dev Unwrap fee for swappers in basis points when swapperFeeWaiverActive is true + uint64 public swapperUnwrapFeeBasisPoints; + + /// @dev Fixed fee in wei for deploying new wrapper pairs + uint64 public deployFee; + + /// @dev Fixed fee in wei for batch confidential transfers + uint64 public batchTransferFee; + + /// @dev Address that receives all collected protocol fees + address public feeRecipient; + + /// @dev When true, addresses with SWAPPER_ROLE pay swapperWrapFeeBasisPoints/swapperUnwrapFeeBasisPoints instead of standard fees + bool public swapperFeeWaiverActive; + + error ZeroAddressFeeRecipient(); + error FeeExceedsMaximum(); + + /// @notice Constructs FeeManager with initial fee configuration + /// @param wrapFeeBasisPoints_ Initial wrap fee in basis points (0-10,000) + /// @param unwrapFeeBasisPoints_ Initial unwrap fee in basis points (0-10,000) + /// @param deployFee_ Initial deployment fee in wei + /// @param batchTransferFee_ Initial batch transfer fee in wei + /// @param feeRecipient_ Address that will receive protocol fees + /// @dev Grants DEFAULT_ADMIN_ROLE to msg.sender + /// @dev Reverts if feeRecipient_ is zero address + /// @dev Reverts if wrap or unwrap fee basis points exceed MAX_BASIS_POINTS + /// @dev Initializes swapper fees to 0 by default + constructor( + uint64 wrapFeeBasisPoints_, + uint64 unwrapFeeBasisPoints_, + uint64 deployFee_, + uint64 batchTransferFee_, + address feeRecipient_ + ) { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + require(feeRecipient_ != address(0), ZeroAddressFeeRecipient()); + require(wrapFeeBasisPoints_ <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + require(unwrapFeeBasisPoints_ <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + wrapFeeBasisPoints = wrapFeeBasisPoints_; + unwrapFeeBasisPoints = unwrapFeeBasisPoints_; + swapperWrapFeeBasisPoints = 0; + swapperUnwrapFeeBasisPoints = 0; + deployFee = deployFee_; + batchTransferFee = batchTransferFee_; + feeRecipient = feeRecipient_; + } + + /// @notice Calculates the wrap fee for a given amount and addresses + /// @param amount The total wrap amount in original token units + /// @param wrapFrom The address performing the wrap operation + /// @param wrapTo The recipient address (unused, reserved for future logic) + /// @return The calculated fee in original token units (rounded up) + /// @dev Uses ceiling division to prevent fee leakage through transaction splitting + /// @dev Returns ceiling of (amount * swapperWrapFeeBasisPoints) / MAX_BASIS_POINTS if swapperFeeWaiverActive is true and wrapFrom has SWAPPER_ROLE + /// @dev Otherwise returns ceiling of (amount * wrapFeeBasisPoints) / MAX_BASIS_POINTS + function getWrapFee(uint256 amount, address wrapFrom, address wrapTo) external view returns (uint256) { + uint64 feeBasisPoints = (swapperFeeWaiverActive && hasRole(SWAPPER_ROLE, wrapFrom)) + ? swapperWrapFeeBasisPoints + : wrapFeeBasisPoints; + + // Ceiling division: (a + b - 1) / b + // This prevents fee leakage from rounding down + uint256 fee = amount * feeBasisPoints; + return (fee + MAX_BASIS_POINTS - 1) / MAX_BASIS_POINTS; + } + + /// @notice Calculates the unwrap fee for a given amount and addresses + /// @param amount The unwrap amount in confidential token units + /// @param unwrapFrom The address initiating unwrap (unused, reserved for future logic) + /// @param unwrapTo The recipient address receiving original tokens + /// @return The calculated fee in confidential token units + /// @dev Returns (amount * swapperUnwrapFeeBasisPoints) / MAX_BASIS_POINTS if swapperFeeWaiverActive is true and unwrapTo has SWAPPER_ROLE + /// @dev Otherwise returns (amount * unwrapFeeBasisPoints) / MAX_BASIS_POINTS + function getUnwrapFee(uint64 amount, address unwrapFrom, address unwrapTo) external view returns (uint64) { + return getFee(amount, getUnwrapFeeBasisPoints(unwrapFrom, unwrapTo)); + } + + /// @notice Returns the unwrap fee basis points for given addresses + /// @param unwrapFrom The address initiating unwrap (unused, reserved for future logic) + /// @param unwrapTo The recipient address receiving original tokens + /// @return The unwrap fee basis points (out of MAX_BASIS_POINTS) + /// @dev Returns swapperUnwrapFeeBasisPoints if swapperFeeWaiverActive is true and unwrapTo has SWAPPER_ROLE + /// @dev Otherwise returns unwrapFeeBasisPoints + /// @dev This method is used by WrapperUpgradeable to commit fee basis points at unwrap initiation, + /// ensuring that fee changes during the unwrap process don't affect in-flight transactions + function getUnwrapFeeBasisPoints(address unwrapFrom, address unwrapTo) public view returns (uint64) { + if (swapperFeeWaiverActive && hasRole(SWAPPER_ROLE, unwrapTo)) { + return swapperUnwrapFeeBasisPoints; + } + return unwrapFeeBasisPoints; + } + + /// @notice Calculates a fee given an amount and basis points + /// @param amount The amount in confidential token units + /// @param basisPoints The fee rate in basis points (e.g., 100 = 1%) + /// @return The calculated fee amount (rounded up) + /// @dev This is a utility function that applies committed basis points to calculate final fees + /// @dev Used by WrapperUpgradeable.finalizeUnwrap to apply committed fee rates + /// @dev Uses ceiling division to prevent fee leakage through transaction splitting + function getFee(uint64 amount, uint64 basisPoints) public view returns (uint64) { + // Ceiling division: (a + b - 1) / b + // This prevents fee leakage from rounding down + uint256 fee = uint256(amount) * uint256(basisPoints); + return uint64((fee + MAX_BASIS_POINTS - 1) / MAX_BASIS_POINTS); + } + + /// @notice Returns the fixed deployment fee + /// @param deployer The address deploying (unused, reserved for future per-deployer logic) + /// @return The deployment fee in wei + function getDeployFee(address deployer) external view returns (uint64) { + return deployFee; + } + + /// @notice Returns the fixed batch transfer fee + /// @return The batch transfer fee in wei + function getBatchTransferFee() external view returns (uint64) { + return batchTransferFee; + } + + /// @notice Returns the current fee recipient address + /// @return The address receiving protocol fees + function getFeeRecipient() external view returns (address) { + return feeRecipient; + } + + /// @notice Updates the wrap fee basis points + /// @param feeBasisPoints New wrap fee in basis points (0-10,000) + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Reverts if feeBasisPoints exceeds MAX_BASIS_POINTS + /// @dev Emits WrapFeeBasisPointsUpdated event + function setWrapFeeBasisPoints(uint64 feeBasisPoints) external onlyRole(FEE_MANAGER_ROLE) { + require(feeBasisPoints <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + uint64 oldFeeBasisPoints = wrapFeeBasisPoints; + wrapFeeBasisPoints = feeBasisPoints; + emit WrapFeeBasisPointsUpdated(oldFeeBasisPoints, feeBasisPoints); + } + + /// @notice Updates the unwrap fee basis points + /// @param feeBasisPoints New unwrap fee in basis points (0-10,000) + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Reverts if feeBasisPoints exceeds MAX_BASIS_POINTS + /// @dev Emits UnwrapFeeBasisPointsUpdated event + function setUnwrapFeeBasisPoints(uint64 feeBasisPoints) external onlyRole(FEE_MANAGER_ROLE) { + require(feeBasisPoints <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + uint64 oldFeeBasisPoints = unwrapFeeBasisPoints; + unwrapFeeBasisPoints = feeBasisPoints; + emit UnwrapFeeBasisPointsUpdated(oldFeeBasisPoints, feeBasisPoints); + } + + /// @notice Updates the swapper wrap fee basis points + /// @param feeBasisPoints New swapper wrap fee in basis points (0-10,000) + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Reverts if feeBasisPoints exceeds MAX_BASIS_POINTS + /// @dev Emits SwapperWrapFeeBasisPointsUpdated event + function setSwapperWrapFeeBasisPoints(uint64 feeBasisPoints) external onlyRole(FEE_MANAGER_ROLE) { + require(feeBasisPoints <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + uint64 oldFeeBasisPoints = swapperWrapFeeBasisPoints; + swapperWrapFeeBasisPoints = feeBasisPoints; + emit SwapperWrapFeeBasisPointsUpdated(oldFeeBasisPoints, feeBasisPoints); + } + + /// @notice Updates the swapper unwrap fee basis points + /// @param feeBasisPoints New swapper unwrap fee in basis points (0-10,000) + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Reverts if feeBasisPoints exceeds MAX_BASIS_POINTS + /// @dev Emits SwapperUnwrapFeeBasisPointsUpdated event + function setSwapperUnwrapFeeBasisPoints(uint64 feeBasisPoints) external onlyRole(FEE_MANAGER_ROLE) { + require(feeBasisPoints <= MAX_BASIS_POINTS, FeeExceedsMaximum()); + uint64 oldFeeBasisPoints = swapperUnwrapFeeBasisPoints; + swapperUnwrapFeeBasisPoints = feeBasisPoints; + emit SwapperUnwrapFeeBasisPointsUpdated(oldFeeBasisPoints, feeBasisPoints); + } + + /// @notice Updates the deployment fee + /// @param newDeployFee New deployment fee in wei + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Emits DeployFeeUpdated event + function setDeployFee(uint64 newDeployFee) external onlyRole(FEE_MANAGER_ROLE) { + uint64 oldDeployFee = deployFee; + deployFee = newDeployFee; + emit DeployFeeUpdated(oldDeployFee, newDeployFee); + } + + /// @notice Updates the batch transfer fee + /// @param newBatchTransferFee New batch transfer fee in wei + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Emits BatchTransferFeeUpdated event + function setBatchTransferFee(uint64 newBatchTransferFee) external onlyRole(FEE_MANAGER_ROLE) { + uint64 oldBatchTransferFee = batchTransferFee; + batchTransferFee = newBatchTransferFee; + emit BatchTransferFeeUpdated(oldBatchTransferFee, newBatchTransferFee); + } + + /// @notice Updates the fee recipient address + /// @param recipient New fee recipient address + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev Reverts if recipient is zero address + /// @dev Emits FeeRecipientUpdated event + function setFeeRecipient(address recipient) external onlyRole(FEE_MANAGER_ROLE) { + require(recipient != address(0), ZeroAddressFeeRecipient()); + address oldRecipient = feeRecipient; + feeRecipient = recipient; + emit FeeRecipientUpdated(oldRecipient, recipient); + } + + /// @notice Enables or disables fee waiver for SWAPPER_ROLE addresses + /// @param active True to enable fee waiver, false to disable + /// @dev Only callable by accounts with FEE_MANAGER_ROLE + /// @dev When active, addresses with SWAPPER_ROLE use swapper{Wrap/Unwrap}FeeBasisPoints to calculate fees + /// @dev Emits SwapperFeeWaiverUpdated event + function setSwapperFeeWaiverActive(bool active) external onlyRole(FEE_MANAGER_ROLE) { + swapperFeeWaiverActive = active; + emit SwapperFeeWaiverUpdated(active); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/admin/SanctionsList.sol b/protocol-contracts/confidential-wrapper/contracts/admin/SanctionsList.sol new file mode 100644 index 0000000000..98eaef1b60 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/admin/SanctionsList.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract SanctionsList is Ownable { + + constructor() Ownable(msg.sender) {} + + mapping(address => bool) private sanctionedAddresses; + + event SanctionedAddress(address indexed addr); + event NonSanctionedAddress(address indexed addr); + event SanctionedAddressesAdded(address[] addrs); + event SanctionedAddressesRemoved(address[] addrs); + + function name() external pure returns (string memory) { + return "Chainalysis sanctions oracle"; + } + + function addToSanctionsList(address[] memory newSanctions) public onlyOwner { + for (uint256 i = 0; i < newSanctions.length; i++) { + sanctionedAddresses[newSanctions[i]] = true; + } + emit SanctionedAddressesAdded(newSanctions); + } + + function removeFromSanctionsList(address[] memory removeSanctions) public onlyOwner { + for (uint256 i = 0; i < removeSanctions.length; i++) { + sanctionedAddresses[removeSanctions[i]] = false; + } + emit SanctionedAddressesRemoved(removeSanctions); + } + + function isSanctioned(address addr) public view returns (bool) { + return sanctionedAddresses[addr] == true ; + } + + function isSanctionedVerbose(address addr) public returns (bool) { + if (isSanctioned(addr)) { + emit SanctionedAddress(addr); + return true; + } else { + emit NonSanctionedAddress(addr); + return false; + } + } + +} diff --git a/protocol-contracts/confidential-wrapper/contracts/batcher/ERC7984TransferBatcher.sol b/protocol-contracts/confidential-wrapper/contracts/batcher/ERC7984TransferBatcher.sol new file mode 100644 index 0000000000..a320012969 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/batcher/ERC7984TransferBatcher.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {FHE, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {AdminProvider} from "../admin/AdminProvider.sol"; +import {FeeManager} from "../admin/FeeManager.sol"; + +/// @title ERC7984TransferBatcher +/// @notice Batch processor for confidential transfers with retry mechanism +/// @dev Enables efficient multi-transfer operations with fee payment and sender tracking +/// @dev Key features: +/// - Single transaction batches multiple confidential transfers +/// - Tracks original sender per transaction for retry authorization +/// - Charges fixed fee per batch (not per transfer) +/// - Supports retry mechanism for failed transfers with original sender verification +/// @custom:security-contact contact@zaiffer.org +contract ERC7984TransferBatcher is ZamaEthereumConfig { + error OnlyOriginalSenderCanRetry(); + error InsufficientFee(); + error EmptyTransferArray(); + error FeeTransferFailed(); + + /// @dev The given holder `holder` is not authorized to spend on behalf of `spender`. + error ERC7984UnauthorizedSpender(address holder, address spender); + + event BatchTransfer(address indexed cToken, address indexed sender, uint256 startTxId, uint256 endTxId, uint256 fee); + event RetryTransfer(address indexed cToken, address indexed sender, uint256 originalTxId, uint256 retryTxId); + + /// @dev AdminProvider contract for accessing fee manager + AdminProvider public immutable adminProvider; + + /// @dev Maps cToken address => txId => original sender address for retry authorization + /// @dev Used to verify that only the original sender can retry a specific transaction + mapping(address cToken => mapping(uint256 txId => address sender)) public txIdToSender; + + /// @notice Constructs ERC7984TransferBatcher with admin provider reference + /// @param adminProvider_ AdminProvider contract for accessing fee manager + constructor(AdminProvider adminProvider_) { + adminProvider = adminProvider_; + } + + /// @notice Input structure for a single confidential transfer within a batch + /// @param to Recipient address for the confidential transfer + /// @param encryptedAmount Encrypted transfer amount (euint64) + /// @param inputProof proof for the encrypted amount + /// @param retryFor Transaction ID being retried (0 for new transfers) + struct ConfidentialTransferInput { + address to; + externalEuint64 encryptedAmount; + bytes inputProof; + uint256 retryFor; + } + + /// @notice Executes a batch of confidential transfers in a single transaction + /// @param cToken The confidential token contract to transfer + /// @param from The address from which tokens will be transferred (must be msg.sender or operator-approved) + /// @param transfers Array of transfer inputs containing recipient, amount, proof, and retry info + /// @dev Charges a fixed batch transfer fee (paid in ETH) + /// @dev Fee amount determined by FeeManager.getBatchTransferFee() + /// @dev Tracks original sender for each transaction ID to enable retry authorization + /// @dev For retries, verifies msg.sender matches original sender via txIdToSender mapping + /// @dev Emits BatchTransfer event with start and end transaction IDs + /// @dev Emits RetryTransfer event for each retry operation + /// @dev Reverts if: + /// - transfers array is empty (EmptyTransferArray) + /// - msg.value doesn't match required fee (InsufficientFee) + /// - fee transfer to recipient fails (FeeTransferFailed) + /// - retry attempted by non-original sender (OnlyOriginalSenderCanRetry) + /// - msg.sender is not an approved operator for `from` address + function confidentialBatchTransfer( + RegulatedERC7984Upgradeable cToken, + address from, + ConfidentialTransferInput[] calldata transfers + ) external payable { + if (transfers.length == 0) { + revert EmptyTransferArray(); + } + + FeeManager feeManager = adminProvider.feeManager(); + uint64 requiredFee = feeManager.getBatchTransferFee(); + + if (msg.value != requiredFee) { + revert InsufficientFee(); + } + + require(cToken.isOperator(from, msg.sender), ERC7984UnauthorizedSpender(from, msg.sender)); + + uint256 startTxId = cToken.nextTxId(); + + for (uint256 i = 0; i < transfers.length; i++) { + if (transfers[i].retryFor != 0) { + require( + txIdToSender[address(cToken)][transfers[i].retryFor] == from, + OnlyOriginalSenderCanRetry() + ); + } + + euint64 amount = FHE.fromExternal( + transfers[i].encryptedAmount, + transfers[i].inputProof + ); + FHE.allowTransient(amount, address(cToken)); + + uint256 currentTxId = cToken.nextTxId(); + cToken.confidentialTransferFrom( + from, + transfers[i].to, + amount + ); + + txIdToSender[address(cToken)][currentTxId] = from; + + if (transfers[i].retryFor != 0) { + emit RetryTransfer(address(cToken), from, transfers[i].retryFor, currentTxId); + } + } + + uint256 endTxId = cToken.nextTxId() - 1; + + if (msg.value > 0) { + address feeRecipient = feeManager.getFeeRecipient(); + (bool success, ) = feeRecipient.call{value: msg.value}(""); + require(success, FeeTransferFailed()); + } + + emit BatchTransfer(address(cToken), from, startTxId, endTxId, requiredFee); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/debug/Debug.sol b/protocol-contracts/confidential-wrapper/contracts/debug/Debug.sol new file mode 100644 index 0000000000..73a3c66780 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/debug/Debug.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "hardhat/console.sol"; +import "@fhevm/solidity/lib/FHE.sol"; +import {ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; + +library Debug { + event Debug(string varType, string varName, bytes value); + + function asEuint64( + string memory varName, + euint64 value + ) internal { + FHE.makePubliclyDecryptable(value); + emit Debug("euint64", varName, abi.encode(euint64.unwrap(value))); + } + + function asAddress( + string memory varName, + address value + ) internal { + emit Debug("address", varName, abi.encode(value)); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/factory/DeploymentCoordinator.sol b/protocol-contracts/confidential-wrapper/contracts/factory/DeploymentCoordinator.sol new file mode 100644 index 0000000000..a87258da0e --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/factory/DeploymentCoordinator.sol @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {WrapperUpgradeable} from "../wrapper/WrapperUpgradeable.sol"; +import {WrapperFactory} from "./WrapperFactory.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {RegulatedERC7984UpgradeableFactory} from "./RegulatedERC7984UpgradeableFactory.sol"; +import {AdminProvider} from "../admin/AdminProvider.sol"; +import {FeeManager} from "../admin/FeeManager.sol"; +import {IDeploymentCoordinator} from "../interfaces/IDeploymentCoordinator.sol"; +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +/// @notice Coordinator that orchestrates deployment using specialized factories +/// @dev Coordinates WrapperFactory and RegulatedERC7984UpgradeableFactory to deploy wrapper pairs +/// @custom:security-contact contact@zaiffer.org +contract DeploymentCoordinator is Ownable2Step { + AdminProvider public adminProvider; + WrapperFactory public wrapperFactory; + RegulatedERC7984UpgradeableFactory public confidentialTokenFactory; + + /// @notice Canonical implementation address for all RegulatedERC7984Upgradeable proxies + address public confidentialTokenImplementation; + + /// @notice Canonical implementation address for all WrapperUpgradeable proxies + address public wrapperImplementation; + + /// @notice Mapping from original token address to deployed wrapper address (for compatibility) + mapping(address originalToken => WrapperUpgradeable wrapper) public deployedWrappers; + + /// @notice Mapping from original token address to deployed confidential token address (for compatibility) + mapping(address originalToken => RegulatedERC7984Upgradeable confidentialToken) public deployedConfidentialTokens; + + error ZeroAddressAdminProvider(); + error ZeroAddressWrapperFactory(); + error ZeroAddressConfidentialTokenFactory(); + error ZeroAddressImplementation(); + error IncorrectDeployFee(); + error WrapperAlreadyExists(); + error FeeTransferFailed(); + error ImplementationNotSet(); + error TokenMustExist(); + + event WrapperPairDeployed( + address indexed originalToken, + address indexed wrapper, + address indexed confidentialToken, + string originalName, + string originalSymbol, + uint8 originalDecimals, + address deployer + ); + + event AdminProviderUpdated(address indexed oldAdminProvider, address indexed newAdminProvider); + event WrapperFactoryUpdated(address indexed oldWrapperFactory, address indexed newWrapperFactory); + event ConfidentialTokenFactoryUpdated(address indexed oldConfidentialTokenFactory, address indexed newConfidentialTokenFactory); + event ConfidentialTokenImplementationUpdated(address indexed oldImplementation, address indexed newImplementation); + event WrapperImplementationUpdated(address indexed oldImplementation, address indexed newImplementation); + + constructor( + AdminProvider adminProvider_, + WrapperFactory wrapperFactory_, + RegulatedERC7984UpgradeableFactory confidentialTokenFactory_ + ) Ownable(msg.sender) { + require(address(adminProvider_) != address(0), ZeroAddressAdminProvider()); + require(address(wrapperFactory_) != address(0), ZeroAddressWrapperFactory()); + require(address(confidentialTokenFactory_) != address(0), ZeroAddressConfidentialTokenFactory()); + + adminProvider = adminProvider_; + wrapperFactory = wrapperFactory_; + confidentialTokenFactory = confidentialTokenFactory_; + } + + /// @notice Deploy a wrapper/cToken pair for a given token (maintains original interface) + /// @param originalToken_ Address of the token to wrap (address(0) for ETH) + /// @return wrapper Address of the deployed wrapper contract + /// @return confidentialToken Address of the deployed confidential token contract + function deploy(address originalToken_) + external + payable + returns (WrapperUpgradeable wrapper, RegulatedERC7984Upgradeable confidentialToken) + { + // Get deploy fee from FeeManager + uint64 requiredFee = _getDeployFee(); + require(msg.value == requiredFee, IncorrectDeployFee()); + require(address(deployedWrappers[originalToken_]) == address(0), WrapperAlreadyExists()); + // Prevent griefing attack: ensure token exists (has code) unless it's ETH (address(0)) + require(originalToken_ == address(0) || originalToken_.code.length > 0, TokenMustExist()); + + // Ensure canonical implementation is deployed + if (confidentialTokenImplementation == address(0)) { + confidentialTokenImplementation = address(new RegulatedERC7984Upgradeable()); + } + require(wrapperImplementation != address(0), ImplementationNotSet()); + + // Deploy confidential token first using factory + string memory originalName; + string memory originalSymbol; + uint8 originalDecimals; + ( + confidentialToken, + originalName, + originalSymbol, + originalDecimals + ) = _deployConfidentialTokenForAsset(originalToken_); + + // Deploy wrapper using factory + wrapper = wrapperFactory.deployWrapper( + wrapperImplementation, + originalToken_, + confidentialToken, + IDeploymentCoordinator(address(this)), + adminProvider.owner() + ); + + // Set wrapper using WRAPPER_SETTER_ROLE (one-time only) + confidentialToken.setWrapper(address(wrapper)); + + // Store mappings for compatibility + deployedWrappers[originalToken_] = wrapper; + deployedConfidentialTokens[originalToken_] = confidentialToken; + + // Transfer deployment fee to fee recipient from FeeManager + if (msg.value > 0) { + address feeRecipient = _getFeeRecipient(); + (bool success, ) = feeRecipient.call{value: msg.value}(""); + require(success, FeeTransferFailed()); + } + + emit WrapperPairDeployed( + originalToken_, + address(wrapper), + address(confidentialToken), + originalName, + originalSymbol, + originalDecimals, + msg.sender + ); + } + + /// @notice Get wrapper address for a token + /// @param originalToken_ Token address (address(0) for ETH) + /// @return Address of the wrapper, or address(0) if not deployed + function getWrapper(address originalToken_) external view returns (address) { + return address(deployedWrappers[originalToken_]); + } + + /// @notice Get confidential token address for a token + /// @param originalToken_ Token address (address(0) for ETH) + /// @return Address of the confidential token, or address(0) if not deployed + function getConfidentialToken(address originalToken_) external view returns (address) { + return address(deployedConfidentialTokens[originalToken_]); + } + + /// @notice Check if wrapper exists for a token + /// @param originalToken_ Token address (address(0) for ETH) + /// @return True if wrapper exists + function wrapperExists(address originalToken_) external view returns (bool) { + return address(deployedWrappers[originalToken_]) != address(0); + } + + /// @notice Deploy confidential token for a given asset + /// @param originalToken_ Address of the original token + /// @return confidentialToken The deployed confidential token + function _deployConfidentialTokenForAsset(address originalToken_) + internal + returns (RegulatedERC7984Upgradeable confidentialToken, string memory originalName, string memory originalSymbol, uint8 originalDecimals) + { + if (originalToken_ != address(0)) { + originalName = _tryGetAssetName(originalToken_); + originalSymbol = _tryGetAssetSymbol(originalToken_); + originalDecimals = _tryGetAssetDecimals(IERC20Metadata(originalToken_)); + } else { + originalName = "Ethereum"; + originalSymbol = "ETH"; + originalDecimals = 18; + } + + uint8 maxDecimals = _maxDecimals(); + uint8 tokenDecimals; + uint256 rate; + + if (originalDecimals > maxDecimals) { + tokenDecimals = maxDecimals; + rate = 10 ** (originalDecimals - maxDecimals); + } else { + tokenDecimals = originalDecimals; + rate = 1; + } + + confidentialToken = confidentialTokenFactory.deployConfidentialToken( + confidentialTokenImplementation, + string.concat("confidential ", originalName), + string.concat("c", originalSymbol), + tokenDecimals, + rate, + originalToken_, + IDeploymentCoordinator(address(this)), + adminProvider.owner(), // admin role goes directly to adminProvider owner + address(this) // coordinator gets WRAPPER_SETTER_ROLE for one-time setWrapper call + ); + } + + /// @notice Get deploy fee from AdminProvider's FeeManager + function _getDeployFee() private view returns (uint64) { + FeeManager feeManager = adminProvider.feeManager(); + return feeManager.getDeployFee(msg.sender); + } + + /// @notice Get fee recipient from AdminProvider's FeeManager + function _getFeeRecipient() private view returns (address) { + FeeManager feeManager = adminProvider.feeManager(); + return feeManager.getFeeRecipient(); + } + + function _fallbackUnderlyingDecimals() private pure returns (uint8) { + return 18; + } + + function _maxDecimals() private pure returns (uint8) { + return 6; + } + + function _tryGetAssetDecimals(IERC20Metadata asset_) private view returns (uint8 assetDecimals) { + (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( + abi.encodeCall(IERC20Metadata.decimals, ()) + ); + if (success && encodedDecimals.length == 32) { + return abi.decode(encodedDecimals, (uint8)); + } + return _fallbackUnderlyingDecimals(); + } + + /// @notice Try to get the token name, handling both string and bytes32 returns + /// @param token_ The token address + /// @return The token name, or address-based fallback (40 chars) if unavailable + function _tryGetAssetName(address token_) private view returns (string memory) { + (bool success, bytes memory result) = token_.staticcall(abi.encodeWithSignature("name()")); + if (success) { + string memory parsed = _parseStringOrBytes32(result); + if (bytes(parsed).length > 0) { + return parsed; + } + } + // Fallback: full address as string (40 hex chars) + return Strings.toHexString(token_); + } + + /// @notice Try to get the token symbol, handling both string and bytes32 returns + /// @param token_ The token address + /// @return The token symbol, or address-based fallback (6 chars) if unavailable + function _tryGetAssetSymbol(address token_) private view returns (string memory) { + (bool success, bytes memory result) = token_.staticcall(abi.encodeWithSignature("symbol()")); + if (success) { + string memory parsed = _parseStringOrBytes32(result); + if (bytes(parsed).length > 0) { + return parsed; + } + } + // Fallback: first 6 chars of address hex string + string memory fullAddress = Strings.toHexString(token_); + return _substring(fullAddress, 0, 8); // "0x" + 6 chars + } + + /// @notice Parse encoded data as either string or bytes32 + /// @param data The encoded return data from name() or symbol() + /// @return The parsed string, or empty string if parsing fails + function _parseStringOrBytes32(bytes memory data) private view returns (string memory) { + // Try to decode as string (dynamic type, length >= 64) + if (data.length >= 64) { + // Attempt string decode - if it's valid UTF-8 string data + try this._externalDecodeString(data) returns (string memory str) { + return str; + } catch { + // Not a valid string encoding + } + } + + // Try to decode as bytes32 (fixed type, length == 32) + if (data.length == 32) { + bytes32 b32 = abi.decode(data, (bytes32)); + return _bytes32ToString(b32); + } + + return ""; + } + + /// @notice External helper to decode string (used with try/catch for safety) + /// @param data The encoded data + /// @return The decoded string + function _externalDecodeString(bytes memory data) external pure returns (string memory) { + return abi.decode(data, (string)); + } + + /// @notice Convert bytes32 to string, removing trailing null bytes + /// @param data The bytes32 data + /// @return The string representation without trailing nulls + function _bytes32ToString(bytes32 data) private pure returns (string memory) { + // Find the actual length (last non-zero byte) + uint256 length = 0; + for (uint256 i = 0; i < 32; i++) { + if (data[i] != 0) { + length = i + 1; + } + } + + bytes memory result = new bytes(length); + for (uint256 i = 0; i < length; i++) { + result[i] = data[i]; + } + return string(result); + } + + /// @notice Extract a substring from a string + /// @param str The source string + /// @param startIndex The starting index (inclusive) + /// @param endIndex The ending index (exclusive) + /// @return The substring + function _substring(string memory str, uint256 startIndex, uint256 endIndex) private pure returns (string memory) { + bytes memory strBytes = bytes(str); + require(endIndex <= strBytes.length && startIndex < endIndex, "Invalid substring indices"); + + bytes memory result = new bytes(endIndex - startIndex); + for (uint256 i = startIndex; i < endIndex; i++) { + result[i - startIndex] = strBytes[i]; + } + return string(result); + } + + /// @notice Accept ownership of the confidential token factory + function acceptConfidentialTokenFactoryOwnership() external onlyOwner { + confidentialTokenFactory.acceptOwnership(); + } + + /// @notice Accept ownership of the wrapper factory + function acceptWrapperFactoryOwnership() external onlyOwner { + wrapperFactory.acceptOwnership(); + } + + /// @notice Set the canonical implementation address for confidential tokens + /// @param implementation_ New implementation address + /// @dev Allows owner to set an externally deployed implementation + function setConfidentialTokenImplementation(address implementation_) external onlyOwner { + require(implementation_ != address(0), ZeroAddressImplementation()); + address oldImplementation = confidentialTokenImplementation; + confidentialTokenImplementation = implementation_; + emit ConfidentialTokenImplementationUpdated(oldImplementation, implementation_); + } + + /// @notice Set the canonical implementation address for wrappers + /// @param implementation_ New implementation address + /// @dev Allows owner to set an externally deployed implementation + function setWrapperImplementation(address implementation_) external onlyOwner { + require(implementation_ != address(0), ZeroAddressImplementation()); + address oldImplementation = wrapperImplementation; + wrapperImplementation = implementation_; + emit WrapperImplementationUpdated(oldImplementation, implementation_); + } + + /// @notice Set the admin provider + /// @param adminProvider_ New admin provider address + function setAdminProvider(AdminProvider adminProvider_) external onlyOwner { + require(address(adminProvider_) != address(0), ZeroAddressAdminProvider()); + address oldAdminProvider = address(adminProvider); + adminProvider = adminProvider_; + emit AdminProviderUpdated(oldAdminProvider, address(adminProvider_)); + } + + /// @notice Set the wrapper factory + /// @param wrapperFactory_ New wrapper factory address + function setWrapperFactory(WrapperFactory wrapperFactory_) external onlyOwner { + require(address(wrapperFactory_) != address(0), ZeroAddressWrapperFactory()); + address oldWrapperFactory = address(wrapperFactory); + wrapperFactory = wrapperFactory_; + emit WrapperFactoryUpdated(oldWrapperFactory, address(wrapperFactory_)); + } + + /// @notice Set the confidential token factory + /// @param confidentialTokenFactory_ New confidential token factory address + function setConfidentialTokenFactory(RegulatedERC7984UpgradeableFactory confidentialTokenFactory_) external onlyOwner { + require(address(confidentialTokenFactory_) != address(0), ZeroAddressConfidentialTokenFactory()); + address oldConfidentialTokenFactory = address(confidentialTokenFactory); + confidentialTokenFactory = confidentialTokenFactory_; + emit ConfidentialTokenFactoryUpdated(oldConfidentialTokenFactory, address(confidentialTokenFactory_)); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/factory/RegulatedERC7984UpgradeableFactory.sol b/protocol-contracts/confidential-wrapper/contracts/factory/RegulatedERC7984UpgradeableFactory.sol new file mode 100644 index 0000000000..175b636e80 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/factory/RegulatedERC7984UpgradeableFactory.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {IDeploymentCoordinator} from "../interfaces/IDeploymentCoordinator.sol"; +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/// @notice Factory contract specialized for deploying RegulatedERC7984Upgradeable tokens +/// @dev Part of the split factory architecture to stay under contract size limits +/// @custom:security-contact contact@zaiffer.org +contract RegulatedERC7984UpgradeableFactory is Ownable2Step { + struct TokenDeploymentParams { + address implementation; + address confidentialToken; + string name; + string symbol; + uint8 decimals; + address underlying; + } + + event ConfidentialTokenDeployed( + address indexed implementation, + address indexed confidentialToken, + string name, + string symbol, + uint8 decimals, + address indexed underlying + ); + + constructor() Ownable(msg.sender) {} + + /// @notice Deploy a confidential token for a given original token + /// @param implementation_ Address of the canonical implementation contract + /// @param name_ Name of the confidential token + /// @param symbol_ Symbol of the confidential token + /// @param decimals_ Decimals of the confidential token + /// @param rate_ Rate for decimal conversion + /// @param underlying_ Address of the underlying token + /// @param deploymentCoordinator_ DeploymentCoordinator address (used to dynamically fetch AdminProvider) + /// @param admin_ Address that will receive DEFAULT_ADMIN_ROLE + /// @param wrapperSetter_ Address that will receive WRAPPER_SETTER_ROLE (typically the coordinator) + /// @return confidentialToken Address of the deployed confidential token + function deployConfidentialToken( + address implementation_, + string calldata name_, + string calldata symbol_, + uint8 decimals_, + uint256 rate_, + address underlying_, + IDeploymentCoordinator deploymentCoordinator_, + address admin_, + address wrapperSetter_ + ) external onlyOwner returns (RegulatedERC7984Upgradeable) { + // Use provided canonical implementation instead of deploying new one + address proxyAddress; + { + bytes memory data = abi.encodeCall( + RegulatedERC7984Upgradeable.initialize, + (name_, symbol_, decimals_, admin_, rate_, underlying_, deploymentCoordinator_, wrapperSetter_) + ); + + ERC1967Proxy proxy = new ERC1967Proxy(implementation_, data); + proxyAddress = address(proxy); + } + + emit ConfidentialTokenDeployed( + implementation_, + proxyAddress, + name_, + symbol_, + decimals_, + underlying_ + ); + + return RegulatedERC7984Upgradeable(proxyAddress); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/factory/WrapperFactory.sol b/protocol-contracts/confidential-wrapper/contracts/factory/WrapperFactory.sol new file mode 100644 index 0000000000..ff5a57e40c --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/factory/WrapperFactory.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {WrapperUpgradeable} from "../wrapper/WrapperUpgradeable.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {IDeploymentCoordinator} from "../interfaces/IDeploymentCoordinator.sol"; +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/// @notice Factory contract specialized for deploying upgradeable Wrapper contracts +/// @dev Part of the split factory architecture to stay under contract size limits +/// @custom:security-contact contact@zaiffer.org +contract WrapperFactory is Ownable2Step { + error ZeroAddressImplementation(); + error ZeroAddressAdmin(); + error ImplementationNotContract(); + + event WrapperDeployed( + address indexed wrapper, + address indexed originalToken, + address indexed confidentialToken, + address implementation, + address deploymentCoordinator, + address admin + ); + + constructor() Ownable(msg.sender) {} + + /// @notice Deploy an upgradeable wrapper for a given original token and confidential token pair + /// @param implementation_ Address of the WrapperUpgradeable implementation contract + /// @param originalToken_ Address of the original token (address(0) for ETH) + /// @param confidentialToken_ Address of the paired confidential token + /// @param deploymentCoordinator_ Address of the deployment coordinator (used to dynamically fetch AdminProvider) + /// @param admin_ Address that will be granted DEFAULT_ADMIN_ROLE on the wrapper + /// @return wrapper Address of the deployed wrapper proxy contract + function deployWrapper( + address implementation_, + address originalToken_, + RegulatedERC7984Upgradeable confidentialToken_, + IDeploymentCoordinator deploymentCoordinator_, + address admin_ + ) external onlyOwner returns (WrapperUpgradeable) { + // Validate implementation address + require(implementation_ != address(0), ZeroAddressImplementation()); + require(_isContract(implementation_), ImplementationNotContract()); + + // Validate admin address + require(admin_ != address(0), ZeroAddressAdmin()); + + // Note: confidentialToken_ and deploymentCoordinator_ are validated in WrapperUpgradeable.initialize() + + bytes memory data = abi.encodeCall( + WrapperUpgradeable.initialize, + (originalToken_, confidentialToken_, deploymentCoordinator_, admin_) + ); + + ERC1967Proxy proxy = new ERC1967Proxy(implementation_, data); + + WrapperUpgradeable wrapper = WrapperUpgradeable(payable(address(proxy))); + + emit WrapperDeployed( + address(wrapper), + originalToken_, + address(confidentialToken_), + implementation_, + address(deploymentCoordinator_), + admin_ + ); + + return wrapper; + } + + /// @notice Check if an address is a contract + /// @param account Address to check + /// @return True if the address contains code, false otherwise + function _isContract(address account) private view returns (bool) { + return account.code.length > 0; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/fhevm/EthereumConfigUpgradeable.sol b/protocol-contracts/confidential-wrapper/contracts/fhevm/EthereumConfigUpgradeable.sol new file mode 100644 index 0000000000..c9aea668b6 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/fhevm/EthereumConfigUpgradeable.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {FHE} from "@fhevm/solidity/lib/FHE.sol"; +import {ZamaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +abstract contract EthereumConfigUpgradeable is Initializable { + function __EthereumConfig_init() internal onlyInitializing { + __EthereumConfig_init_unchained(); + } + + function __EthereumConfig_init_unchained() internal onlyInitializing { + FHE.setCoprocessor(ZamaConfig.getEthereumCoprocessorConfig()); + } + + function confidentialProtocolId() public view returns (uint256) { + return ZamaConfig.getConfidentialProtocolId(); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/interfaces/IDeploymentCoordinator.sol b/protocol-contracts/confidential-wrapper/contracts/interfaces/IDeploymentCoordinator.sol new file mode 100644 index 0000000000..33f5855173 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/interfaces/IDeploymentCoordinator.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {AdminProvider} from "../admin/AdminProvider.sol"; + +/// @title IDeploymentCoordinator +/// @notice Interface for accessing DeploymentCoordinator configuration +/// @dev Used by WrapperUpgradeable and RegulatedERC7984Upgradeable to dynamically retrieve AdminProvider +interface IDeploymentCoordinator { + /// @notice Returns the current AdminProvider address + /// @return The AdminProvider contract instance + function adminProvider() external view returns (AdminProvider); +} diff --git a/protocol-contracts/confidential-wrapper/contracts/interfaces/IERC20.sol b/protocol-contracts/confidential-wrapper/contracts/interfaces/IERC20.sol new file mode 100644 index 0000000000..0fbe2d9138 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/interfaces/IERC20.sol @@ -0,0 +1,12 @@ +pragma solidity 0.8.27; + + +interface IERC20 { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function balanceOf(address) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/protocol-contracts/confidential-wrapper/contracts/interfaces/IUniswap.sol b/protocol-contracts/confidential-wrapper/contracts/interfaces/IUniswap.sol new file mode 100644 index 0000000000..d093688e0d --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/interfaces/IUniswap.sol @@ -0,0 +1,138 @@ +pragma solidity 0.8.27; + + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} + + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} diff --git a/protocol-contracts/confidential-wrapper/contracts/interfaces/IWrapperReceiver.sol b/protocol-contracts/confidential-wrapper/contracts/interfaces/IWrapperReceiver.sol new file mode 100644 index 0000000000..d4102db023 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/interfaces/IWrapperReceiver.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; + +/// @dev Interface for contracts that can receive callbacks after a Wrapper unwrap is finalized. +/// @notice Implementing this interface allows contracts to be notified when an unwrap completes, +/// enabling composable operations like swaps or other token transformations. +interface IWrapperReceiver { + /** + * @dev Called by the Wrapper after an unwrap operation is finalized and underlying tokens are transferred. + * @notice This callback is invoked AFTER the underlying tokens have been sent to the receiver. + * @param operator The address that initiated the finalization + * @param amount The amount of underlying tokens that were unwrapped + * @param unwrapRequestId The unique identifier for this unwrap request + * @param data Arbitrary callback data that was provided during the original unwrap request + * @return A boolean indicating success of the callback operation + */ + function onUnwrapFinalizedReceived( + address operator, + uint256 amount, + uint256 unwrapRequestId, + address refundTo, + bytes calldata data + ) external returns (bool); +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/BurnableRegulatedERC7984Upgradeable.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/BurnableRegulatedERC7984Upgradeable.sol new file mode 100644 index 0000000000..935e49e9a7 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/BurnableRegulatedERC7984Upgradeable.sol @@ -0,0 +1,14 @@ +pragma solidity 0.8.27; + +import {FHE, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; + + +contract BurnableRegulatedERC7984Upgradeable is RegulatedERC7984Upgradeable { + function burn( + externalEuint64 amount, + bytes calldata inputProof + ) public virtual onlyRole(WRAPPER_ROLE) returns (euint64) { + return burn(FHE.fromExternal(amount, inputProof), msg.sender); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20NoDecimals.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20NoDecimals.sol new file mode 100644 index 0000000000..1a1468c841 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20NoDecimals.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20NoDecimals is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { + _mint(msg.sender, 1000000 * 10**18); + } + + // Override decimals to revert, simulating a token without decimals function + function decimals() public pure override returns (uint8) { + revert("decimals not implemented"); + } +} + +contract ERC20InvalidDecimals is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { + _mint(msg.sender, 1000000 * 10**18); + } + + // Override decimals to return invalid data (not 32 bytes) + function decimals() public pure override returns (uint8) { + assembly { + // Return 64 bytes instead of 32 + mstore(0x00, 0x12) + mstore(0x20, 0x34) + return(0x00, 0x40) + } + } +} \ No newline at end of file diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20WithCallback.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20WithCallback.sol new file mode 100644 index 0000000000..1937ead28c --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC20WithCallback.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {WrapperUpgradeable} from "../wrapper/WrapperUpgradeable.sol"; + +/// @notice Interface for tokens that call back to the sender +interface ITransferCallback { + function onTokenTransfer(address from, address to, uint256 amount) external; +} + +/// @notice Mock ERC20 token with transfer callback functionality for testing reentrancy protection +/// @dev Calls back to the sender during transferFrom to enable reentrancy testing +contract ERC20WithCallback is ERC20 { + uint8 private _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_) { + _decimals = decimals_; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + /// @dev Override transferFrom to add callback functionality + function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + + // Callback to 'from' address BEFORE transfer (like ERC777 tokensToSend) + // This enables reentrancy attacks + if (from.code.length > 0) { + try ITransferCallback(from).onTokenTransfer(from, to, value) { + // Callback succeeded + } catch { + // Callback failed, continue with transfer + } + } + + _transfer(from, to, value); + return true; + } +} + +/// @notice Malicious contract that attempts reentrancy during token transfer callbacks +/// @dev Implements callback to reenter wrap() during token transfers +contract ReentrancyAttacker is ITransferCallback { + WrapperUpgradeable public wrapper; + ERC20WithCallback public token; + address public attacker; + uint256 public attackCount; + uint256 public reentrancyAttempts; + uint256 public maxAttacks; + bool public attacking; + uint256 public wrapAmount; + + event ReentrancyAttempted(uint256 attemptNumber, bool success, string reason); + event AttackStarted(uint256 maxAttacks); + event AttackEnded(uint256 totalAttempts); + + constructor(WrapperUpgradeable wrapper_, ERC20WithCallback token_) { + wrapper = wrapper_; + token = token_; + attacker = msg.sender; + } + + /// @notice Start a reentrancy attack + /// @param amount Amount to wrap + /// @param maxAttacks_ Maximum number of reentrancy attempts (1 = no reentrancy, 2+ = reentrancy) + function attack(uint256 amount, uint256 maxAttacks_) external { + require(!attacking, "Attack already in progress"); + + attacking = true; + attackCount = 0; + reentrancyAttempts = 0; + maxAttacks = maxAttacks_; + wrapAmount = amount; + + emit AttackStarted(maxAttacks_); + + // Approve wrapper to spend tokens + token.approve(address(wrapper), type(uint256).max); + + // Start the wrap - this will trigger onTokenTransfer callback + wrapper.wrap(address(this), amount); + + attacking = false; + emit AttackEnded(attackCount); + } + + /// @notice Callback function - called during token transfer + /// @dev This is where the reentrancy attack happens + function onTokenTransfer( + address from, + address /* to */, + uint256 /* amount */ + ) external override { + // Increment callback counter + attackCount++; + + // Only reenter if we're in an attack, the callback is from our token, + // and we haven't exceeded max reentrancy attempts + // maxAttacks = 1 means no reentrancy (just count the call) + // maxAttacks > 1 means attempt reentrancy up to (maxAttacks - 1) times + if (attacking && msg.sender == address(token) && from == address(this) && reentrancyAttempts < (maxAttacks - 1)) { + reentrancyAttempts++; + + // Attempt to reenter wrap() - this should fail with ReentrancyGuard + try wrapper.wrap(address(this), wrapAmount) { + emit ReentrancyAttempted(reentrancyAttempts, true, "Reentrancy succeeded"); + } catch Error(string memory reason) { + emit ReentrancyAttempted(reentrancyAttempts, false, reason); + } catch (bytes memory) { + emit ReentrancyAttempted(reentrancyAttempts, false, "Reentrancy failed"); + } + } + } + + /// @notice Withdraw tokens from this contract + function withdraw() external { + uint256 balance = token.balanceOf(address(this)); + if (balance > 0) { + token.transfer(msg.sender, balance); + } + } + + /// @notice Receive function to accept ETH + receive() external payable {} +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984ReceiverMock.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984ReceiverMock.sol new file mode 100644 index 0000000000..9ef70c1703 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984ReceiverMock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Source: https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-confidential-contracts/3a8689021a89e5d9c3101280cb2cd9435cbf28f1/contracts/mocks/token/ERC7984ReceiverMock.sol + +pragma solidity 0.8.27; + +import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {IERC7984Receiver} from "openzeppelin-confidential-contracts/contracts/interfaces/IERC7984Receiver.sol"; + +contract ERC7984ReceiverMock is IERC7984Receiver, ZamaEthereumConfig { + event ConfidentialTransferCallback(bool success); + + error InvalidInput(uint8 input); + + /// Data should contain a success boolean (plaintext). Revert if not. + function onConfidentialTransferReceived(address, address, euint64, bytes calldata data) external returns (ebool) { + uint8 input = abi.decode(data, (uint8)); + + if (input > 1) revert InvalidInput(input); + + bool success = input == 1; + emit ConfidentialTransferCallback(success); + + ebool returnVal = FHE.asEbool(success); + FHE.allowTransient(returnVal, msg.sender); + + return returnVal; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984UpgradeableMock.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984UpgradeableMock.sol new file mode 100644 index 0000000000..70879eaaca --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ERC7984UpgradeableMock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "hardhat/console.sol"; +import {EthereumConfigUpgradeable} from "../fhevm/EthereumConfigUpgradeable.sol"; +import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {ERC7984Upgradeable} from "../token/ERC7984Upgradeable.sol"; + +contract ERC7984MockUpgradeable is EthereumConfigUpgradeable, ERC7984Upgradeable { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(string memory name_, string memory symbol_, string memory tokenURI_) public initializer { + __EthereumConfig_init(); + __ERC7984_init(name_, symbol_, tokenURI_); + } + + function mint( + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public returns (euint64 transferred) { + return _mint(to, FHE.fromExternal(encryptedAmount, inputProof)); + } + + function burn( + address from, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public returns (euint64 transferred) { + return _burn(from, FHE.fromExternal(encryptedAmount, inputProof)); + } + + function transfer( + address from, + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public returns (euint64 transferred) { + return _transfer(from, to, FHE.fromExternal(encryptedAmount, inputProof)); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/Erc20Mintable.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/Erc20Mintable.sol new file mode 100644 index 0000000000..e6209a7aca --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/Erc20Mintable.sol @@ -0,0 +1,19 @@ +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20 is ERC20 { + uint8 public _decimals; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) { + _decimals = decimals_; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/FeeOnTransferERC20.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/FeeOnTransferERC20.sol new file mode 100644 index 0000000000..db57cbfe57 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/FeeOnTransferERC20.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @notice Mock ERC20 token that charges a fee on every transfer +/// @dev Used for testing wrapper behavior with fee-on-transfer tokens +contract FeeOnTransferERC20 is ERC20 { + uint8 public _decimals; + uint256 public transferFeeBasisPoints; // 10_000 == 100% + address public feeCollector; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + uint256 transferFeeBasisPoints_ + ) ERC20(name_, symbol_) { + _decimals = decimals_; + transferFeeBasisPoints = transferFeeBasisPoints_; + feeCollector = msg.sender; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function setTransferFeeBasisPoints(uint256 feeBasisPoints) external { + require(feeBasisPoints <= 10_000, "Fee too high"); + transferFeeBasisPoints = feeBasisPoints; + } + + function setFeeCollector(address collector) external { + feeCollector = collector; + } + + function _update(address from, address to, uint256 value) internal virtual override { + if (from == address(0) || to == address(0)) { + // No fee on mint/burn + super._update(from, to, value); + return; + } + + // Calculate fee + uint256 fee = (value * transferFeeBasisPoints) / 10_000; + uint256 amountAfterFee = value - fee; + + // Transfer the amount minus fee + super._update(from, to, amountAfterFee); + + // Transfer fee to collector + if (fee > 0) { + super._update(from, feeCollector, fee); + } + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapFactory.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapFactory.sol new file mode 100644 index 0000000000..06b711b917 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapFactory.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: + +pragma solidity =0.5.16; + +import "@uniswap/v2-core/contracts/UniswapV2Factory.sol"; + +contract ForceCompilation is UniswapV2Factory { +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapRouter.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapRouter.sol new file mode 100644 index 0000000000..f380592380 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/ForceCompileUniswapRouter.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: + +pragma solidity =0.6.6; + +import "@uniswap/v2-periphery/contracts/UniswapV2Router02.sol"; + +abstract contract ForceCompilation is UniswapV2Router02 { +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapper.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapper.sol new file mode 100644 index 0000000000..66f11b0a57 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapper.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; +import {SwapData} from "../swap/swap_v0.sol"; + +/// @title MaliciousWrapper +/// @notice A malicious wrapper implementation that attempts to bypass fees +/// @dev This contract demonstrates an attack where an unauthorized contract tries to +/// call SwapV0's onUnwrapFinalizedReceived directly to bypass fees +contract MaliciousWrapper { + using SafeERC20 for IERC20; + + address public immutable originalToken; + address public immutable swapV0; + + constructor(address originalToken_, address swapV0_) { + originalToken = originalToken_; + swapV0 = swapV0_; + } + + /// @notice Attempt to exploit SwapV0 by bypassing wrapper verification + /// @dev This function attempts to: + /// 1. Take tokens from the caller + /// 2. Transfer them to SwapV0 + /// 3. Call onUnwrapFinalizedReceived pretending to be a legitimate wrapper + /// 4. Should be blocked if SwapV0 properly validates msg.sender + function attemptExploit( + uint256 amount, + address router, + uint256 amountOutMin, + address[] memory path, + uint256 deadline, + address to + ) external { + // Receive tokens from caller + IERC20(originalToken).safeTransferFrom(msg.sender, address(this), amount); + + // Transfer tokens to SwapV0 + IERC20(originalToken).safeTransfer(swapV0, amount); + + // Encode the swap parameters + SwapData memory swapData = SwapData(router, amountOutMin, path, deadline, to); + bytes memory data = abi.encode(swapData); + + // Attempt to invoke onUnwrapFinalizedReceived directly + // This attack would work should the swapper not validate msg.sender is a legitimate + // wrapper registered with the coordinator. The attack exploits the fact that: + // 1. checkPath only verifies that a wrapper exists for originalToken in the coordinator + // 2. This malicious contract implements the required originalToken() function + // 3. Without msg.sender validation, any contract can pretend to be a wrapper + IWrapperReceiver(swapV0).onUnwrapFinalizedReceived( + msg.sender, + amount, + 0, // unwrapRequestId + msg.sender, + data + ); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapperAttacker.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapperAttacker.sol new file mode 100644 index 0000000000..5ef6599625 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/MaliciousWrapperAttacker.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; +import {SwapData} from "../swap/swap_v0.sol"; + +/// @title MockConfidentialToken +/// @notice Mock confidential token that only implements nextTxId for testing +contract MockConfidentialToken { + uint256 public nextTxId = 1; +} + +/// @title MaliciousWrapperAttacker +/// @notice Demonstrates the wrapper verification bypass attack +/// @dev This contract exploits the vulnerability where SwapV0 doesn't verify msg.sender +/// is a legitimate wrapper before processing refunds +contract MaliciousWrapperAttacker { + using SafeERC20 for IERC20; + + address public immutable targetToken; + address public immutable swapV0Address; + address public owner; + MockConfidentialToken public mockCToken; + + // Track stolen tokens + uint256 public stolenAmount; + + constructor(address targetToken_, address swapV0_) { + targetToken = targetToken_; + swapV0Address = swapV0_; + owner = msg.sender; + mockCToken = new MockConfidentialToken(); + } + + /// @notice Returns the token this "wrapper" claims to wrap + /// @dev This is called by SwapV0.checkPath and SwapV0._refundUser + function originalToken() external view returns (address) { + return targetToken; + } + + /// @notice Returns a mock confidential token + /// @dev Required for _refundUser to call confidentialToken().nextTxId() + function confidentialToken() external view returns (MockConfidentialToken) { + return mockCToken; + } + + /// @notice Malicious wrap function that steals approved tokens + /// @dev This is called by SwapV0._refundUser after it approves tokens to this contract + /// @param /* to */ Ignored + /// @param /* amount */ Ignored + function wrap(address /* to */, uint256 /* amount */) external { + // Steal all approved tokens + uint256 allowance = IERC20(targetToken).allowance(swapV0Address, address(this)); + if (allowance > 0) { + IERC20(targetToken).safeTransferFrom(swapV0Address, owner, allowance); + stolenAmount += allowance; + } + } + + /// @notice Initiates the attack by calling SwapV0.onUnwrapFinalizedReceived + /// @dev The attack works by: + /// 1. Providing a path where path[0] != originalToken() to trigger checkPath failure + /// 2. checkPath returns false BEFORE verifying wrapper legitimacy + /// 3. SwapV0 calls _refundUser, which trusts msg.sender and approves tokens + /// 4. Our malicious wrap() function steals the approved tokens + function executeAttack( + uint256 amountIn, + address router, + address[] memory pathWithWrongFirstToken + ) external { + // Prepare swap data with invalid path (path[0] != targetToken) + // This will cause checkPath to return false before wrapper validation + SwapData memory swapData = SwapData({ + routerAddress: router, + amountOutMin: 0, + path: pathWithWrongFirstToken, + deadline: block.timestamp + 1000, + to: owner + }); + + bytes memory data = abi.encode(swapData); + + // Call SwapV0's callback function directly + // Without proper msg.sender validation, SwapV0 will: + // 1. Call checkPath → returns false (path mismatch) + // 2. Call _refundUser → approves tokens to us and calls our wrap() + // 3. Our wrap() steals the tokens + IWrapperReceiver(swapV0Address).onUnwrapFinalizedReceived( + msg.sender, + amountIn, + 0, // unwrapRequestId + owner, // refundTo + data + ); + } + + /// @notice Allows owner to withdraw stolen tokens + function withdrawStolen() external { + require(msg.sender == owner, "Only owner"); + uint256 balance = IERC20(targetToken).balanceOf(address(this)); + if (balance > 0) { + IERC20(targetToken).safeTransfer(owner, balance); + } + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/RegulatedERC7984UpgradeableV2.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/RegulatedERC7984UpgradeableV2.sol new file mode 100644 index 0000000000..1c0617f520 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/RegulatedERC7984UpgradeableV2.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; + +contract RegulatedERC7984UpgradeableV2Mock is RegulatedERC7984Upgradeable { + /// @custom:storage-location erc7201:zaiffer.storage.RegulatedERC7984V2 + struct RegulatedERC7984V2Storage { + uint64 counter; + } + + // keccak256(abi.encode(uint256(keccak256("zaiffer.storage.RegulatedERC7984V2")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RegulatedERC7984V2StorageLocation = + 0x190df7ff214060257e3b73924d834d06c896c5f08a7237d4c441916b0d675500; + + function _getRegulatedERC7984StorageV2() internal pure returns (RegulatedERC7984V2Storage storage $) { + assembly { + $.slot := RegulatedERC7984V2StorageLocation + } + } + + function incrementCounter() public returns (uint64) { + RegulatedERC7984V2Storage storage $ = _getRegulatedERC7984StorageV2(); + $.counter += 1; + return $.counter; + } + + function counter() public view returns (uint64) { + RegulatedERC7984V2Storage storage $ = _getRegulatedERC7984StorageV2(); + return $.counter; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/RejectEth.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/RejectEth.sol new file mode 100644 index 0000000000..2cd1c6b707 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/RejectEth.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +contract RejectEth { + receive() external payable { + revert("ETH transfers rejected"); + } + + fallback() external payable { + revert("ETH transfers rejected"); + } +} \ No newline at end of file diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/UnsafeErc20Mintable.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/UnsafeErc20Mintable.sol new file mode 100644 index 0000000000..d8a39592a3 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/UnsafeErc20Mintable.sol @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {TestERC20} from "./Erc20Mintable.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC-20 + * applications. + */ +abstract contract UnsafeERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { + mapping(address account => uint256) private _balances; + + mapping(address account => mapping(address spender => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * Both values are immutable: they can only be set once during construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /// @inheritdoc IERC20 + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /// @inheritdoc IERC20 + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /// @inheritdoc IERC20 + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Skips emitting an {Approval} event indicating an allowance update. This is not + * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve]. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * + * ```solidity + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner`'s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance < type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} + + +contract TestUnsafeERC20 is UnsafeERC20 { + uint8 public _decimals; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) UnsafeERC20(name_, symbol_) { + _decimals = decimals_; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } +} + +contract ERC20ReturnFalseMock is TestERC20 { + constructor(string memory name_, string memory symbol_, uint8 decimals_) TestERC20(name_, symbol_, decimals_) {} + + function transferFrom(address, address, uint256) public pure override returns (bool) { + return false; + } + + function transfer(address, uint256) public pure override returns (bool) { + return false; + } + + function approve(address, uint256) public pure override returns (bool) { + return false; + } +} + +/// @notice Mock that fails when transferring to specific addresses (used to test transfer failure branches) +/// @dev Set the fail addresses after deployment to control which transfers fail +contract ERC20FailOnAddressMock is TestERC20 { + mapping(address => bool) public failOnTransferTo; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) TestERC20(name_, symbol_, decimals_) {} + + function setFailOnTransferTo(address _failAddress, bool _shouldFail) external { + failOnTransferTo[_failAddress] = _shouldFail; + } + + function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { + if (failOnTransferTo[to]) { + return false; + } + return super.transferFrom(from, to, value); + } + + function transfer(address to, uint256 value) public virtual override returns (bool) { + if (failOnTransferTo[to]) { + return false; + } + return super.transfer(to, value); + } +} + +/// @notice Mock that reverts when transferring to specific addresses (used to test transfer failure branches) +/// @dev Set the fail addresses after deployment to control which transfers fail +contract ERC20RevertOnAddressMock is ERC20FailOnAddressMock { + error TransferFailed(); + + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20FailOnAddressMock(name_, symbol_, decimals_) {} + + function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { + if (failOnTransferTo[to]) { + revert TransferFailed(); + } + return super.transferFrom(from, to, value); + } + + function transfer(address to, uint256 value) public virtual override returns (bool) { + if (failOnTransferTo[to]) { + revert TransferFailed(); + } + return super.transfer(to, value); + } +} + +/// @notice Mock that mimics USDT approval behavior (requires 0-approval before changing to non-zero) +/// @dev Reverts if trying to approve non-zero value when allowance is already non-zero +contract ERC20USDTApprovalMock is TestERC20 { + error ApprovalFromNonZeroToNonZero(); + + constructor(string memory name_, string memory symbol_, uint8 decimals_) TestERC20(name_, symbol_, decimals_) {} + + function approve(address spender, uint256 value) public override returns (bool) { + uint256 currentAllowance = allowance(msg.sender, spender); + + // Revert if trying to set non-zero approval when current allowance is non-zero + if (currentAllowance != 0 && value != 0) { + revert ApprovalFromNonZeroToNonZero(); + } + + return super.approve(spender, value); + } + + /// @notice Test helper to arbitrarily set approval state for testing forceApprove + /// @dev Bypasses the USDT approval restriction to set up test scenarios + function testSetApproval(address owner, address spender, uint256 value) external { + _approve(owner, spender, value); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/WETH9.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/WETH9.sol new file mode 100644 index 0000000000..f5ef5b49eb --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/WETH9.sol @@ -0,0 +1,757 @@ +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.8.27; + + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping (address => uint) public balanceOf; + mapping (address => mapping (address => uint)) public allowance; + + // function() public payable { + // deposit(); + // } + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad, ""); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) + public + returns (bool) + { + require(balanceOf[src] >= wad, ""); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) { + require(allowance[src][msg.sender] >= wad, ""); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} + + +/* + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +*/ diff --git a/protocol-contracts/confidential-wrapper/contracts/mocks/WrapperReceiverMock.sol b/protocol-contracts/confidential-wrapper/contracts/mocks/WrapperReceiverMock.sol new file mode 100644 index 0000000000..9a614ca400 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/mocks/WrapperReceiverMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; + +/// @notice Mock contract for testing IWrapperReceiver callback functionality +/// @dev Allows configuration of return value for testing both success and failure cases +contract WrapperReceiverMock is IWrapperReceiver { + bool public returnValue = true; + + // Track callback invocations for testing + uint256 public callbackCount; + address public lastOperator; + uint256 public lastAmount; + uint256 public lastUnwrapRequestId; + address public lastRefundTo; + bytes public lastData; + + /// @notice Set the return value for subsequent callback calls + function setReturnValue(bool _returnValue) external { + returnValue = _returnValue; + } + + /// @notice Implementation of IWrapperReceiver callback + function onUnwrapFinalizedReceived( + address operator, + uint256 amount, + uint256 unwrapRequestId, + address refundTo, + bytes calldata data + ) external override returns (bool) { + callbackCount++; + lastOperator = operator; + lastAmount = amount; + lastUnwrapRequestId = unwrapRequestId; + lastRefundTo = refundTo; + lastData = data; + + return returnValue; + } + + /// @notice Reset tracking state for fresh test + function reset() external { + callbackCount = 0; + lastOperator = address(0); + lastAmount = 0; + lastUnwrapRequestId = 0; + lastRefundTo = address(0); + lastData = ""; + returnValue = true; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/swap/swap_v0.sol b/protocol-contracts/confidential-wrapper/contracts/swap/swap_v0.sol new file mode 100644 index 0000000000..c1e8879984 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/swap/swap_v0.sol @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.27; + +import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {IERC7984Receiver} from "openzeppelin-confidential-contracts/contracts/interfaces/IERC7984Receiver.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IUniswapV2Router02} from "../interfaces/IUniswap.sol"; + +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; +import {DeploymentCoordinator} from "../factory/DeploymentCoordinator.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {WrapperUpgradeable} from "../wrapper/WrapperUpgradeable.sol"; + + +struct SwapData { + address routerAddress; + uint256 amountOutMin; + address[] path; + uint256 deadline; + address to; +} + + +/// @title SwapV0 +/// @notice Enables atomic swaps between confidential tokens via Uniswap V2 +/// @dev Implements IWrapperReceiver to receive unwrapped tokens and execute swaps +/// @dev Flow: User initiates confidentialTransferAndCall → unwrap → swap → wrap → refund on failure +/// @dev Key features: +/// - Validates swap paths before execution +/// - Automatic refund to confidential tokens on any failure +/// - Only accepts unwraps from DeploymentCoordinator-registered wrappers +/// - Uses forceApprove for USDT-like token compatibility +/// - Router whitelisting managed by SwapV0 owner (Ownable2Step) +/// @custom:security-contact contact@zaiffer.org +contract SwapV0 is IWrapperReceiver, Ownable2Step { + using SafeERC20 for IERC20; + + /// @dev DeploymentCoordinator for validating wrapper registrations + DeploymentCoordinator public coordinator; + + /// @dev Mapping of whitelisted router addresses (address => bool) + mapping(address => bool) public whitelistedRouters; + + /// @dev Mapping of whitelisted token addresses for swap paths (address => bool) + mapping(address => bool) public whitelistedTokens; + + /// @dev Error code: Path[0] must match confidential token's underlying token + bytes public constant INPUT_PATH_IS_NOT_UNDERLYING = abi.encodePacked(keccak256("INPUT_PATH_IS_NOT_UNDERLYING")); + + /// @dev Error code: Input token must have a registered wrapper + bytes public constant INPUT_PATH_HAS_NO_WRAPPER = abi.encodePacked(keccak256("INPUT_PATH_HAS_NO_WRAPPER")); + + /// @dev Error code: Output token must have a registered wrapper + bytes public constant OUTPUT_PATH_HAS_NO_WRAPPER = abi.encodePacked(keccak256("OUTPUT_PATH_HAS_NO_WRAPPER")); + + /// @dev Error code: Recipient address cannot be zero address + bytes public constant RECIPIENT_CANNOT_BE_ZERO_ADDRESS = abi.encodePacked(keccak256("RECIPIENT_CANNOT_BE_ZERO_ADDRESS")); + + /// @dev Error code: Router address is not whitelisted + bytes public constant ROUTER_NOT_WHITELISTED = abi.encodePacked(keccak256("ROUTER_NOT_WHITELISTED")); + + /// @dev Error code: Token in swap path is not whitelisted + bytes public constant TOKEN_NOT_WHITELISTED = abi.encodePacked(keccak256("TOKEN_NOT_WHITELISTED")); + + /// @dev Error code: Recipient is sanctioned + bytes public constant RECIPIENT_IS_SANCTIONED = abi.encodePacked(keccak256("RECIPIENT_IS_SANCTIONED")); + + /// @dev Error code: Refund address is sanctioned + bytes public constant REFUND_ADDRESS_IS_SANCTIONED = abi.encodePacked(keccak256("REFUND_ADDRESS_IS_SANCTIONED")); + + error UnknownWrapper(); + error ZeroAddressRouter(); + error RouterAlreadyWhitelisted(); + error RouterNotWhitelisted(); + error ZeroAddressToken(); + error TokenAlreadyWhitelisted(); + error TokenNotWhitelisted(); + error ZeroAddressRecipient(); + error TokenTransferFailed(); + + /// @notice Emitted after swap attempt (successful or failed) + /// @param success True if swap succeeded, false if refunded + /// @param path Uniswap swap path (token addresses) + /// @param unwrapRequestId Original unwrap request ID from wrapper + /// @param wrapTxId Transaction ID of output token wrap (0 if failed) + /// @param errorReasonString Error string if swap failed + /// @param errorLowLevelData Low-level error data if swap failed + event Swap( + bool indexed success, + address[] path, + uint256 indexed unwrapRequestId, + uint256 wrapTxId, + string errorReasonString, + bytes errorLowLevelData + ); + + /// @notice Emitted when router is added to whitelist + /// @param router Router address added to whitelist + event RouterAddedToWhitelist(address indexed router); + + /// @notice Emitted when router is removed from whitelist + /// @param router Router address removed from whitelist + event RouterRemovedFromWhitelist(address indexed router); + + /// @notice Emitted when token is added to whitelist + /// @param token Token address added to whitelist + event TokenAddedToWhitelist(address indexed token); + + /// @notice Emitted when token is removed from whitelist + /// @param token Token address removed from whitelist + event TokenRemovedFromWhitelist(address indexed token); + + /// @notice Emitted when tokens are rescued by owner + /// @param token Token address rescued (address(0) for ETH) + /// @param to Recipient address + /// @param amount Amount rescued + event TokensRescued(address indexed token, address indexed to, uint256 amount); + + /// @notice Constructs SwapV0 with deployment coordinator reference + /// @param coordinator_ DeploymentCoordinator for wrapper validation + constructor(DeploymentCoordinator coordinator_) Ownable(msg.sender) { + coordinator = coordinator_; + } + + /// @notice Receives ETH from wrapper unwraps and Uniswap swaps + /// @dev Required to support ETH swaps (cETH → token or token → cETH) + receive() external payable {} + + /// @notice Adds a router address to the whitelist + /// @param router Router address to whitelist + /// @dev Only callable by owner + /// @dev Reverts if router is zero address + /// @dev Reverts if router is already whitelisted + /// @dev Emits RouterAddedToWhitelist event + function addRouterToWhitelist(address router) external onlyOwner { + require(router != address(0), ZeroAddressRouter()); + require(!whitelistedRouters[router], RouterAlreadyWhitelisted()); + whitelistedRouters[router] = true; + emit RouterAddedToWhitelist(router); + } + + /// @notice Removes a router address from the whitelist + /// @param router Router address to remove + /// @dev Only callable by owner + /// @dev Reverts if router is zero address + /// @dev Reverts if router is not currently whitelisted + /// @dev Emits RouterRemovedFromWhitelist event + function removeRouterFromWhitelist(address router) external onlyOwner { + require(router != address(0), ZeroAddressRouter()); + require(whitelistedRouters[router], RouterNotWhitelisted()); + whitelistedRouters[router] = false; + emit RouterRemovedFromWhitelist(router); + } + + /// @notice Adds a token address to the whitelist + /// @param token Token address to whitelist + /// @dev Only callable by owner + /// @dev Reverts if token is zero address + /// @dev Reverts if token is already whitelisted + /// @dev Emits TokenAddedToWhitelist event + function addTokenToWhitelist(address token) external onlyOwner { + require(token != address(0), ZeroAddressToken()); + require(!whitelistedTokens[token], TokenAlreadyWhitelisted()); + whitelistedTokens[token] = true; + emit TokenAddedToWhitelist(token); + } + + /// @notice Removes a token address from the whitelist + /// @param token Token address to remove + /// @dev Only callable by owner + /// @dev Reverts if token is zero address + /// @dev Reverts if token is not currently whitelisted + /// @dev Emits TokenRemovedFromWhitelist event + function removeTokenFromWhitelist(address token) external onlyOwner { + require(token != address(0), ZeroAddressToken()); + require(whitelistedTokens[token], TokenNotWhitelisted()); + whitelistedTokens[token] = false; + emit TokenRemovedFromWhitelist(token); + } + + /// @notice Rescues stuck tokens from the contract + /// @param token Token address to rescue (address(0) for ETH) + /// @param to Recipient address + /// @param amount Amount to rescue + /// @dev Only callable by owner (governance) + /// @dev Reverts if recipient is zero address + /// @dev Reverts if token transfer fails + /// @dev Emits TokensRescued event + /// @dev This function is a safety mechanism to recover tokens that may get stuck in the contract + /// due to failed swaps or other unforeseen circumstances + function rescueTokens(address token, address to, uint256 amount) external onlyOwner { + require(to != address(0), ZeroAddressRecipient()); + + if (token == address(0)) { + // Rescue ETH + (bool success, ) = to.call{value: amount}(""); + require(success, TokenTransferFailed()); + } else { + // Rescue ERC20 tokens + IERC20(token).safeTransfer(to, amount); + } + + emit TokensRescued(token, to, amount); + } + + /// @notice Validates a swap path for execution + /// @param wrapper The wrapper that initiated the unwrap + /// @param router Uniswap router address for path verification + /// @param path Array of token addresses defining the swap route + /// @return success True if path is valid, false otherwise + /// @return errorReasonString Error string if validation failed + /// @return errorLowLevelData Low-level error data if validation failed + /// @dev Performs six critical validations: + /// 1. Router whitelist: whitelistedRouters[router] == true + /// 2. Token whitelist: all tokens in path must be whitelisted + /// 3. Underlying token match: For ETH, wrapper.originalToken() == address(0) and path[0] == WETH + /// 4. Input wrapper exists: coordinator.deployedWrappers(underlyingOrWeth) != address(0) + /// 5. Output wrapper exists: coordinator.deployedWrappers(outputUnderlyingOrWeth) != address(0) + /// 6. Wrapper consistency: wrapper == coordinator.deployedWrappers(underlyingAddress) + function checkPath(WrapperUpgradeable wrapper, address router, address[] memory path) public view returns (bool, string memory, bytes memory) { + address underlyingAddress = wrapper.originalToken(); + WrapperUpgradeable wrapperIn = coordinator.deployedWrappers(underlyingAddress); + + // check if router is whitelisted + if (!whitelistedRouters[router]) { + return (false, "", ROUTER_NOT_WHITELISTED); + } + + // check if all tokens in the path are whitelisted + for (uint256 i = 0; i < path.length; i++) { + if (!whitelistedTokens[path[i]]) { + return (false, "", TOKEN_NOT_WHITELISTED); + } + } + + address weth = IUniswapV2Router02(router).WETH(); + + // For ETH swaps, the underlying is address(0) but Uniswap path uses WETH + // Input validation: ETH → Token swap + if (underlyingAddress == address(0)) { + // ETH input: path[0] must be WETH + if (path[0] != weth) { + return (false, "", INPUT_PATH_IS_NOT_UNDERLYING); + } + } else { + // ERC20 input: path[0] must match underlying exactly + if (underlyingAddress != path[0]) { + return (false, "", INPUT_PATH_IS_NOT_UNDERLYING); + } + } + + // does a wrapper exist for input token with the configured coordinator + // this ensures that only unwrapping from coordinator wrappers can interact with this contract + if (address(wrapperIn) == address(0)) { + return (false, "", INPUT_PATH_HAS_NO_WRAPPER); + } + + // Output validation: check if wrapper exists for output token + // For Token → ETH swaps, path[last] is WETH but we need wrapper for address(0) + address outputToken = path[path.length - 1]; + address outputUnderlying = (outputToken == weth) ? address(0) : outputToken; + + WrapperUpgradeable wrapperOut = coordinator.deployedWrappers(outputUnderlying); + if (address(wrapperOut) == address(0)) { + return (false, "", OUTPUT_PATH_HAS_NO_WRAPPER); + } + + return (true, "", new bytes(0)); + } + + /// @notice Validates unwrap inputs and refunds user if path is invalid + /// @param swapData Uniswap swap data + /// @param amountIn Amount of input tokens unwrapped + /// @param refundTo Address to refund to should the inputs be invalid + /// @param unwrapRequestId Unwrap request ID for event tracking + /// @return True if validation passed, false if refund was issued + /// @dev Internal helper to validate path and automatically refund on failure + function _validateUnwrapFinalizedReceivedInputsOrRefund( + SwapData memory swapData, + uint256 amountIn, + address refundTo, + uint256 unwrapRequestId + ) internal returns (bool) { + if(coordinator.adminProvider().sanctionsList().isSanctioned(swapData.to)) { + if(coordinator.adminProvider().sanctionsList().isSanctioned(refundTo)) { + address feeRecipient = address(coordinator.adminProvider().feeManager().feeRecipient()); + _refundUser(swapData.routerAddress, swapData.path, amountIn, feeRecipient, unwrapRequestId, "", REFUND_ADDRESS_IS_SANCTIONED); + } else { + _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, "", RECIPIENT_IS_SANCTIONED); + } + return false; + } + + // Validate recipient address is not zero address + // This prevents funds from being stuck when wrap() would revert on minting to address(0) + if (swapData.to == address(0)) { + _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, "", RECIPIENT_CANNOT_BE_ZERO_ADDRESS); + return false; + } + + ( + bool isValidPath, + string memory errorString, + bytes memory errorLowLevelData + ) = checkPath(WrapperUpgradeable(payable(msg.sender)), swapData.routerAddress, swapData.path); + + if (!isValidPath) { + _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, errorString, errorLowLevelData); + return false; + } + + return true; + } + + /// @notice Callback invoked by wrapper when unwrap is finalized + /// @param amountIn Amount of original tokens received from unwrap + /// @param unwrapRequestId Unwrap request ID for tracking + /// @param data ABI-encoded swap parameters: (router, amountOutMin, path, deadline, to) + /// @return ebool Success indicator (encrypted bool) for wrapper to track swap status + /// @dev Implements IWrapperReceiver interface + /// @dev Flow on success: Validate path → Approve router → Swap on Uniswap → Wrap output → Emit Swap event + /// @dev Flow on failure: Validate path → Refund by wrapping input tokens back → Emit Swap event with error + /// @dev Supports three swap types: + /// 1. Token → Token: swapExactTokensForTokens + /// 2. ETH → Token: swapExactETHForTokens (path[0] = WETH, receives ETH) + /// 3. Token → ETH: swapExactTokensForETH (path[last] = WETH, outputs ETH) + /// @dev Uses forceApprove for USDT-like token compatibility + /// @dev Returns encrypted bool to allow wrapper to track success/failure on-chain + function onUnwrapFinalizedReceived( + address /* operator */, + uint256 amountIn, + uint256 unwrapRequestId, + address refundTo, + bytes calldata data + ) external returns (bool) { + WrapperUpgradeable wrapper = WrapperUpgradeable(payable(msg.sender)); + address underlyingAddress = wrapper.originalToken(); + WrapperUpgradeable wrapperIn = coordinator.deployedWrappers(underlyingAddress); + + // is the wrapper that called onUnwrapFinalizedReceived the same as the one that's + // associated with the underlying at the coordinator level. + require(address(wrapper) == address(wrapperIn), UnknownWrapper()); + + SwapData memory swapData = abi.decode(data, (SwapData)); + bool returnVal = true; + + if (!_validateUnwrapFinalizedReceivedInputsOrRefund(swapData, amountIn, refundTo, unwrapRequestId)) { + return false; + } + + returnVal = _executeSwap(swapData, amountIn, refundTo, unwrapRequestId); + + return returnVal; + } + + /// @notice Executes the appropriate Uniswap swap based on input/output types + /// @param swapData Swap parameters + /// @param amountIn Amount of input tokens + /// @param refundTo Address to refund on failure + /// @param unwrapRequestId Unwrap request ID + /// @return bool True if swap succeeded, false if failed + /// @dev Internal helper to execute ETH or ERC20 swaps + function _executeSwap( + SwapData memory swapData, + uint256 amountIn, + address refundTo, + uint256 unwrapRequestId + ) internal returns (bool) { + address weth = IUniswapV2Router02(swapData.routerAddress).WETH(); + + // ETH → Token swap + if (swapData.path[0] == weth && address(this).balance >= amountIn) { + try IUniswapV2Router02(swapData.routerAddress).swapExactETHForTokens{value: amountIn}( + swapData.amountOutMin, + swapData.path, + address(this), + swapData.deadline + ) returns (uint256[] memory amounts) { + _handleUniswapSuccess(swapData.routerAddress, swapData.path, amounts, swapData.to, refundTo, unwrapRequestId); + return true; + } catch Error(string memory reason) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, reason, new bytes(0)); + } catch (bytes memory lowLevelData) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, "", lowLevelData); + } + } + + // Token → ETH swap + if (swapData.path[swapData.path.length - 1] == weth) { + IERC20(swapData.path[0]).forceApprove(swapData.routerAddress, amountIn); + try IUniswapV2Router02(swapData.routerAddress).swapExactTokensForETH( + amountIn, + swapData.amountOutMin, + swapData.path, + address(this), + swapData.deadline + ) returns (uint256[] memory amounts) { + _handleUniswapSuccess(swapData.routerAddress, swapData.path, amounts, swapData.to, refundTo, unwrapRequestId); + return true; + } catch Error(string memory reason) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, reason, new bytes(0)); + } catch (bytes memory lowLevelData) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, "", lowLevelData); + } + } + + // Token → Token swap + IERC20(swapData.path[0]).forceApprove(swapData.routerAddress, amountIn); + try IUniswapV2Router02(swapData.routerAddress).swapExactTokensForTokens( + amountIn, + swapData.amountOutMin, + swapData.path, + address(this), + swapData.deadline + ) returns (uint256[] memory amounts) { + _handleUniswapSuccess(swapData.routerAddress, swapData.path, amounts, swapData.to, refundTo, unwrapRequestId); + return true; + } catch Error(string memory reason) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, reason, new bytes(0)); + } catch (bytes memory lowLevelData) { + return _refundUser(swapData.routerAddress, swapData.path, amountIn, refundTo, unwrapRequestId, "", lowLevelData); + } + } + + /// @notice Handles successful Uniswap swap by wrapping output tokens + /// @param router Uniswap router address (for WETH lookup) + /// @param path Swap path array + /// @param amounts Output amounts from Uniswap swap + /// @param to Recipient address for wrapped output tokens + /// @param unwrapRequestId Original unwrap request ID + /// @dev Internal helper called when Uniswap swap succeeds + /// @dev For ERC20 output: Approves output wrapper → Wraps output tokens + /// @dev For ETH output: Sends ETH value to wrapper.wrap() + /// @dev Emits success Swap event + function _handleUniswapSuccess(address router, address[] memory path, uint256[] memory amounts, address to, address refundTo, uint256 unwrapRequestId) private { + address tokenOut = path[path.length - 1]; + uint256 amountOut = amounts[amounts.length - 1]; + address weth = IUniswapV2Router02(router).WETH(); + + // Determine the actual underlying token (ETH is address(0), not WETH) + address outputUnderlying = (tokenOut == weth) ? address(0) : tokenOut; + WrapperUpgradeable wrapperOut = coordinator.deployedWrappers(outputUnderlying); + + RegulatedERC7984Upgradeable cTokenOut = wrapperOut.confidentialToken(); + uint256 wrapTxId = cTokenOut.nextTxId(); + + if (tokenOut == weth) { + // ETH output: send ETH value to wrapper + wrapperOut.wrap{value: amountOut}(to, amountOut); + } else { + // ERC20 output: approve and wrap + IERC20(tokenOut).forceApprove(address(wrapperOut), amountOut); + wrapperOut.wrap(to, amountOut); + } + + emit Swap(true, path, unwrapRequestId, wrapTxId, "", new bytes(0)); + } + + /// @notice Refunds user by re-wrapping input tokens when swap fails + /// @param router Uniswap router address (to reset approval and get WETH address) + /// @param path Swap path array + /// @param amountIn Amount of input tokens expected (may differ from actual balance for FOT tokens) + /// @param to Recipient address for refunded confidential tokens + /// @param unwrapRequestId Original unwrap request ID + /// @param errorReasonString Error string from failed operation + /// @param errorLowLevelData Low-level error data from failed operation + /// @return ebool Encrypted false to indicate failure to wrapper + /// @dev Internal helper called on any swap failure (path validation or Uniswap execution) + /// @dev For ERC20: Resets router approval → Approves wrapper → Wraps tokens back + /// @dev For ETH: Sends ETH value to wrapper.wrap() + /// @dev For fee-on-transfer tokens, uses actual balance instead of amountIn to prevent revert + /// @dev Emits failure Swap event + function _refundUser( + address router, + address[] memory path, + uint256 amountIn, + address to, + uint256 unwrapRequestId, + string memory errorReasonString, + bytes memory errorLowLevelData + ) internal returns (bool) { + WrapperUpgradeable wrapperIn = WrapperUpgradeable(payable(msg.sender)); + + uint256 wrapTxId = wrapperIn.confidentialToken().nextTxId(); + + // ETH refund + if (wrapperIn.originalToken() == address(0)) { + uint256 refundAmount = address(this).balance < amountIn ? address(this).balance : amountIn; + wrapperIn.wrap{value: refundAmount}(to, refundAmount); + } else { + // ERC20 refund: handle fee-on-transfer tokens + uint256 balance = IERC20(wrapperIn.originalToken()).balanceOf(address(this)); + uint256 refundAmount = balance < amountIn ? balance : amountIn; + IERC20(wrapperIn.originalToken()).forceApprove(router, 0); + IERC20(wrapperIn.originalToken()).forceApprove(address(wrapperIn), refundAmount); + wrapperIn.wrap(to, refundAmount); + } + + emit Swap(false, path, unwrapRequestId, wrapTxId, errorReasonString, errorLowLevelData); + + return false; + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/token/ERC7984Upgradeable.sol b/protocol-contracts/confidential-wrapper/contracts/token/ERC7984Upgradeable.sol new file mode 100644 index 0000000000..517f47e80c --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/token/ERC7984Upgradeable.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: MIT +// ported openzeppelin-confidential-contracts/contracts/token/ERC7984/ERC7984.sol::ERC7984 +// so that it is upgradeable +pragma solidity 0.8.27; + +import {FHE, externalEuint64, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {FHESafeMath} from "openzeppelin-confidential-contracts/contracts/utils/FHESafeMath.sol"; +import {ERC7984Utils} from "openzeppelin-confidential-contracts/contracts/token/ERC7984/utils/ERC7984Utils.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {IERC7984} from "openzeppelin-confidential-contracts/contracts/interfaces/IERC7984.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + + +/** + * @dev Reference implementation for {IERC7984}. + * + * This contract implements a fungible token where balances and transfers are encrypted using the Zama fhEVM, + * providing confidentiality to users. Token amounts are stored as encrypted, unsigned integers (`euint64`) + * that can only be decrypted by authorized parties. + * + * Key features: + * + * - All balances are encrypted + * - Transfers happen without revealing amounts + * - Support for operators (delegated transfer capabilities with time bounds) + * - Transfer and call pattern + * - Safe overflow/underflow handling for FHE operations + * @custom:security-contact contact@zaiffer.org + */ +abstract contract ERC7984Upgradeable is Initializable, ContextUpgradeable, IERC7984, ERC165 { + /// @custom:storage-location erc7201:zaiffer.storage.ERC7984 + struct ERC7984Storage { + mapping(address holder => euint64 balance) _balances; + mapping(address holder => mapping(address operator => uint48 validUntilTimestamp)) _operators; + euint64 _totalSupply; + string _name; + string _symbol; + string _contractURI; + } + + /// @dev Emitted when an encrypted amount `encryptedAmount` is requested for disclosure by `requester`. + event AmountDiscloseRequested(euint64 indexed encryptedAmount, address indexed requester); + + /// @dev The given receiver `receiver` is invalid for transfers. + error ERC7984InvalidReceiver(address receiver); + + /// @dev The given sender `sender` is invalid for transfers. + error ERC7984InvalidSender(address sender); + + /// @dev The given holder `holder` is not authorized to spend on behalf of `spender`. + error ERC7984UnauthorizedSpender(address holder, address spender); + + /// @dev The holder `holder` is trying to send tokens but has a balance of 0. + error ERC7984ZeroBalance(address holder); + + /** + * @dev The caller `user` does not have access to the encrypted amount `amount`. + * + * NOTE: Try using the equivalent transfer function with an input proof. + */ + error ERC7984UnauthorizedUseOfEncryptedAmount(euint64 amount, address user); + + /// @dev The given gateway request ID `requestId` is invalid. + error ERC7984InvalidGatewayRequest(uint256 requestId); + + // keccak256(abi.encode(uint256(keccak256("zaiffer.storage.ERC7984")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ERC7984StorageLocation = + 0xd12b0a7bdf03cc96abdbbaf7f50ef0849fae327c4a79f7c69a7a543c83e24100; + + function _getERC7984Storage() internal pure returns (ERC7984Storage storage $) { + assembly { + $.slot := ERC7984StorageLocation + } + } + + function __ERC7984_init( + string memory name_, + string memory symbol_, + string memory contractURI_ + ) internal onlyInitializing { + __ERC7984_init_unchained(name_, symbol_, contractURI_); + } + + function __ERC7984_init_unchained( + string memory name_, + string memory symbol_, + string memory contractURI_ + ) internal onlyInitializing { + ERC7984Storage storage $ = _getERC7984Storage(); + $._name = name_; + $._symbol = symbol_; + $._contractURI = contractURI_; + } + + /// @inheritdoc IERC7984 + function name() public view virtual returns (string memory) { + ERC7984Storage storage $ = _getERC7984Storage(); + return $._name; + } + + /// @inheritdoc IERC7984 + function symbol() public view virtual returns (string memory) { + ERC7984Storage storage $ = _getERC7984Storage(); + return $._symbol; + } + + /// @inheritdoc IERC7984 + function decimals() public view virtual returns (uint8) { + return 6; + } + + /// @inheritdoc IERC7984 + function contractURI() public view virtual returns (string memory) { + ERC7984Storage storage $ = _getERC7984Storage(); + return $._contractURI; + } + + /// @inheritdoc IERC7984 + function confidentialTotalSupply() public view virtual returns (euint64) { + ERC7984Storage storage $ = _getERC7984Storage(); + return $._totalSupply; + } + + function _setTotalSupply(euint64 totalSupply) internal virtual { + ERC7984Storage storage $ = _getERC7984Storage(); + $._totalSupply = totalSupply; + } + + /// @inheritdoc IERC7984 + function confidentialBalanceOf(address account) public view virtual returns (euint64) { + ERC7984Storage storage $ = _getERC7984Storage(); + return $._balances[account]; + } + + /// @inheritdoc IERC7984 + function isOperator(address holder, address spender) public view virtual returns (bool) { + ERC7984Storage storage $ = _getERC7984Storage(); + return holder == spender || block.timestamp <= $._operators[holder][spender]; + } + + /// @inheritdoc IERC7984 + function setOperator(address operator, uint48 until) public virtual { + _setOperator(_msgSender(), operator, until); + } + + /// @inheritdoc IERC7984 + function confidentialTransfer( + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public virtual returns (euint64) { + return _transfer(_msgSender(), to, FHE.fromExternal(encryptedAmount, inputProof)); + } + + /// @inheritdoc IERC7984 + function confidentialTransfer(address to, euint64 amount) public virtual returns (euint64) { + require(FHE.isAllowed(amount, _msgSender()), ERC7984UnauthorizedUseOfEncryptedAmount(amount, _msgSender())); + return _transfer(_msgSender(), to, amount); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFrom( + address from, + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public virtual returns (euint64 transferred) { + require(isOperator(from, _msgSender()), ERC7984UnauthorizedSpender(from, _msgSender())); + transferred = _transfer(from, to, FHE.fromExternal(encryptedAmount, inputProof)); + FHE.allowTransient(transferred, _msgSender()); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFrom( + address from, + address to, + euint64 amount + ) public virtual returns (euint64 transferred) { + require(FHE.isAllowed(amount, _msgSender()), ERC7984UnauthorizedUseOfEncryptedAmount(amount, _msgSender())); + require(isOperator(from, _msgSender()), ERC7984UnauthorizedSpender(from, _msgSender())); + transferred = _transfer(from, to, amount); + FHE.allowTransient(transferred, _msgSender()); + } + + /// @inheritdoc IERC7984 + function confidentialTransferAndCall( + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof, + bytes calldata data + ) public virtual returns (euint64 transferred) { + transferred = _transferAndCall(_msgSender(), to, FHE.fromExternal(encryptedAmount, inputProof), data); + FHE.allowTransient(transferred, _msgSender()); + } + + /// @inheritdoc IERC7984 + function confidentialTransferAndCall( + address to, + euint64 amount, + bytes calldata data + ) public virtual returns (euint64 transferred) { + require(FHE.isAllowed(amount, _msgSender()), ERC7984UnauthorizedUseOfEncryptedAmount(amount, _msgSender())); + transferred = _transferAndCall(_msgSender(), to, amount, data); + FHE.allowTransient(transferred, _msgSender()); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFromAndCall( + address from, + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof, + bytes calldata data + ) public virtual returns (euint64 transferred) { + require(isOperator(from, _msgSender()), ERC7984UnauthorizedSpender(from, _msgSender())); + transferred = _transferAndCall(from, to, FHE.fromExternal(encryptedAmount, inputProof), data); + FHE.allowTransient(transferred, _msgSender()); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFromAndCall( + address from, + address to, + euint64 amount, + bytes calldata data + ) public virtual returns (euint64 transferred) { + require(FHE.isAllowed(amount, _msgSender()), ERC7984UnauthorizedUseOfEncryptedAmount(amount, _msgSender())); + require(isOperator(from, _msgSender()), ERC7984UnauthorizedSpender(from, _msgSender())); + transferred = _transferAndCall(from, to, amount, data); + FHE.allowTransient(transferred, _msgSender()); + } + + /** + * @dev Starts the process to disclose an encrypted amount `encryptedAmount` publicly by making it + * publicly decryptable. Emits the {AmountDiscloseRequested} event. + * + * NOTE: Both `msg.sender` and `address(this)` must have permission to access the encrypted amount + * `encryptedAmount` to request disclosure of the encrypted amount `encryptedAmount`. + */ + function requestDiscloseEncryptedAmount(euint64 encryptedAmount) public virtual { + require( + FHE.isAllowed(encryptedAmount, _msgSender()), + ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, _msgSender()) + ); + + FHE.makePubliclyDecryptable(encryptedAmount); + emit AmountDiscloseRequested(encryptedAmount, _msgSender()); + } + + /** + * @dev Publicly discloses an encrypted value with a given decryption proof. Emits the {AmountDisclosed} event. + * + * NOTE: May not be tied to a prior request via {requestDiscloseEncryptedAmount}. + */ + function discloseEncryptedAmount( + euint64 encryptedAmount, + uint64 cleartextAmount, + bytes calldata decryptionProof + ) public virtual { + bytes32[] memory handles = new bytes32[](1); + handles[0] = euint64.unwrap(encryptedAmount); + + bytes memory cleartextMemory = abi.encode(cleartextAmount); + + FHE.checkSignatures(handles, cleartextMemory, decryptionProof); + emit AmountDisclosed(encryptedAmount, cleartextAmount); + } + + function _setOperator(address holder, address operator, uint48 until) internal virtual { + ERC7984Storage storage $ = _getERC7984Storage(); + $._operators[holder][operator] = until; + emit OperatorSet(holder, operator, until); + } + + function _mint(address to, euint64 amount) internal returns (euint64) { + require(to != address(0), ERC7984InvalidReceiver(address(0))); + return _update(address(0), to, amount); + } + + function _burn(address from, euint64 amount) internal returns (euint64 transferred) { + require(from != address(0), ERC7984InvalidSender(address(0))); + return _update(from, address(0), amount); + } + + function _transfer(address from, address to, euint64 amount) internal virtual returns (euint64 transferred) { + require(from != address(0), ERC7984InvalidSender(address(0))); + require(to != address(0), ERC7984InvalidReceiver(address(0))); + return _update(from, to, amount); + } + + function _transferAndCall( + address from, + address to, + euint64 amount, + bytes calldata data + ) internal returns (euint64 transferred) { + // Try to transfer amount + replace input with actually transferred amount. + euint64 sent = _transfer(from, to, amount); + + // Perform callback + ebool success = ERC7984Utils.checkOnTransferReceived(_msgSender(), from, to, sent, data); + + // Try to refund if callback fails + euint64 refund = _update(to, from, FHE.select(success, FHE.asEuint64(0), sent)); + transferred = FHE.sub(sent, refund); + } + + function _update(address from, address to, euint64 amount) internal virtual returns (euint64 transferred) { + ERC7984Storage storage $ = _getERC7984Storage(); + + ebool success; + euint64 ptr; + + if (from == address(0)) { + (success, ptr) = FHESafeMath.tryIncrease($._totalSupply, amount); + FHE.allowThis(ptr); + _setTotalSupply(ptr); + } else { + euint64 fromBalance = $._balances[from]; + require(FHE.isInitialized(fromBalance), ERC7984ZeroBalance(from)); + (success, ptr) = FHESafeMath.tryDecrease(fromBalance, amount); + FHE.allowThis(ptr); + FHE.allow(ptr, from); + $._balances[from] = ptr; + } + + transferred = FHE.select(success, amount, FHE.asEuint64(0)); + + if (to == address(0)) { + ptr = FHE.sub($._totalSupply, transferred); + FHE.allowThis(ptr); + _setTotalSupply(ptr); + } else { + ptr = FHE.add($._balances[to], transferred); + FHE.allowThis(ptr); + FHE.allow(ptr, to); + $._balances[to] = ptr; + } + + if (from != address(0)) FHE.allow(transferred, from); + if (to != address(0)) FHE.allow(transferred, to); + FHE.allowThis(transferred); + emit ConfidentialTransfer(from, to, transferred); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IERC7984).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/token/RegulatedERC7984Upgradeable.sol b/protocol-contracts/confidential-wrapper/contracts/token/RegulatedERC7984Upgradeable.sol new file mode 100644 index 0000000000..41bcaf7486 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/token/RegulatedERC7984Upgradeable.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {FHE, ebool, euint64 } from "@fhevm/solidity/lib/FHE.sol"; +import {EthereumConfigUpgradeable} from "../fhevm/EthereumConfigUpgradeable.sol"; +import {ERC7984Upgradeable} from "./ERC7984Upgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {SanctionsList} from "../admin/SanctionsList.sol"; +import {AdminProvider} from "../admin/AdminProvider.sol"; +import {IDeploymentCoordinator} from "../interfaces/IDeploymentCoordinator.sol"; + +interface FHEErrors { + /** + * @notice Returned when an address is sanctioned. + */ + error SanctionedAddress(address account); + + /** + * @notice Returned when rate parameter is invalid. + */ + error InvalidRate(); + + /** + * @notice Returned when wrapper has already been set. + */ + error WrapperAlreadySet(); + + /** + * @notice FHE.isSenderAllowed(encryptedValue) returned false + */ + error SenderNotAllowed(); +} + +/// @custom:security-contact contact@zaiffer.org +contract RegulatedERC7984Upgradeable is + EthereumConfigUpgradeable, + AccessControlDefaultAdminRulesUpgradeable, + ERC7984Upgradeable, + UUPSUpgradeable, + IERC20Errors, + FHEErrors +{ + bytes32 public constant WRAPPER_ROLE = keccak256("WRAPPER_ROLE"); + bytes32 public constant WRAPPER_SETTER_ROLE = keccak256("WRAPPER_SETTER_ROLE"); + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /// @custom:storage-location erc7201:zaiffer.storage.RegulatedERC7984 + struct RegulatedERC7984Storage { + uint256 _rate; + uint256 _nextTxId; + address _underlying; + uint8 _decimals; + bool _wrapperSet; + IDeploymentCoordinator _deploymentCoordinator; + address _tokenRegulator; + } + + event TransferInfo(address indexed from, address indexed to, euint64 encryptedAmount, uint256 txId); + event MintInfo(address indexed to, uint64 amount, uint256 txId); + event BurnInfo(address indexed from, euint64 amount, uint256 txId); + event TokenRegulatorUpdated(address indexed oldRegulator, address indexed newRegulator); + + // keccak256(abi.encode(uint256(keccak256("zaiffer.storage.RegulatedERC7984")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RegulatedERC7984StorageLocation = + 0x825f52e2e3e00f0b167e52d69cfe92f4ec69b657f7c0aaefb00a7607a3ca0100; + + function _getRegulatedERC7984Storage() internal pure returns (RegulatedERC7984Storage storage $) { + assembly { + $.slot := RegulatedERC7984StorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name_, + string memory symbol_, + uint8 decimals_, + address admin_, + uint256 rate_, + address underlying_, + IDeploymentCoordinator deploymentCoordinator_, + address wrapperSetter_ + ) public initializer { + __EthereumConfig_init(); + __AccessControlDefaultAdminRules_init(0, admin_); // 0 delay for admin transfer + __ERC7984_init(name_, symbol_, ""); + + _grantRole(WRAPPER_SETTER_ROLE, wrapperSetter_); + + // Validate rate bounds + // For tokens with decimals <= 6: rate must be 1 + // For tokens with decimals > 6: rate = 10^(decimals - 6) + // Maximum supported is 30 decimals, so max rate = 10^24 + require(rate_ >= 1 && rate_ <= 10**24, InvalidRate()); + + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + $._decimals = decimals_; + $._rate = rate_; + $._deploymentCoordinator = deploymentCoordinator_; + $._nextTxId = 1; + $._underlying = underlying_; + + _setTotalSupply(FHE.asEuint64(0)); + } + + function _incrementNextTxId() internal { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + $._nextTxId += 1; + } + + function nextTxId() public view returns (uint256) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + return $._nextTxId; + } + + function underlying() public view returns (address) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._underlying; + } + + function decimals() public view override returns (uint8) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._decimals; + } + + function rate() public view returns (uint256) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._rate; + } + + function deploymentCoordinator() public view virtual returns (IDeploymentCoordinator) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._deploymentCoordinator; + } + + function adminProvider() public view virtual returns (AdminProvider) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._deploymentCoordinator.adminProvider(); + } + + function regulator() public view virtual returns (address) { + return adminProvider().regulator(); + } + + function tokenRegulator() public view virtual returns (address) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + return $._tokenRegulator; + } + + function setTokenRegulator(address newRegulator) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + address oldRegulator = $._tokenRegulator; + $._tokenRegulator = newRegulator; + + emit TokenRegulatorUpdated(oldRegulator, newRegulator); + } + + function burn(euint64 amount, address proxyingFor) public virtual onlyRole(WRAPPER_ROLE) returns (euint64) { + require(FHE.isSenderAllowed(amount), SenderNotAllowed()); + address from = _msgSender(); + uint256 txId = nextTxId(); + + euint64 balance = confidentialBalanceOf(from); + ebool isBurnable = FHE.le(amount, balance); + euint64 burnAmount = FHE.select(isBurnable, amount, FHE.asEuint64(0)); + + euint64 burned = _burn(from, burnAmount); + + FHE.allow(burned, proxyingFor); + + emit BurnInfo(proxyingFor, burned, txId); + + return burned; + } + + function mint(address to, uint64 amount) public virtual onlyRole(WRAPPER_ROLE) { + uint256 txId = nextTxId(); + + _mint(to, FHE.asEuint64(amount)); + + emit MintInfo(to, amount, txId); + } + + /// @notice Set the wrapper address and grant it WRAPPER_ROLE (can only be called once) + /// @param wrapper_ Address of the wrapper contract + function setWrapper(address wrapper_) external onlyRole(WRAPPER_SETTER_ROLE) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + if ($._wrapperSet) { + revert WrapperAlreadySet(); + } + + $._wrapperSet = true; + _grantRole(WRAPPER_ROLE, wrapper_); + } + + function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {} + + /** + * @notice Check if an address is sanctioned. + * @param account The address to check. + */ + function _checkSanctions(SanctionsList sanctionsList, address account) internal view { + if (sanctionsList.isSanctioned(account)) { + revert SanctionedAddress(account); + } + } + + // Overrides + + function _setTotalSupply(euint64 totalSupply) internal virtual override { + ERC7984Storage storage $ = _getERC7984Storage(); + $._totalSupply = totalSupply; + FHE.allowThis($._totalSupply); + FHE.makePubliclyDecryptable($._totalSupply); + } + + function _update(address from, address to, euint64 amount) internal override returns (euint64 transferred) { + RegulatedERC7984Storage storage $ = _getRegulatedERC7984Storage(); + + transferred = super._update(from, to, amount); + + SanctionsList sanctionsList = adminProvider().sanctionsList(); + address cachedRegulator = regulator(); + address cachedTokenRegulator = $._tokenRegulator; + + // Check sanctions on msg.sender (operator) - prevents sanctioned addresses from operating on behalf of others + address sender = _msgSender(); + if (sender != from) { + _checkSanctions(sanctionsList, sender); + } + + // Grant admin access to transferred amount and updated balances + if (from != address(0)) { + _checkSanctions(sanctionsList, from); + euint64 fromBalance = confidentialBalanceOf(from); + FHE.allow(fromBalance, cachedRegulator); + if (cachedTokenRegulator != address(0)) { + FHE.allow(fromBalance, cachedTokenRegulator); + } + } + if (to != address(0)) { + _checkSanctions(sanctionsList, to); + euint64 toBalance = confidentialBalanceOf(to); + FHE.allow(toBalance, cachedRegulator); + if (cachedTokenRegulator != address(0)) { + FHE.allow(toBalance, cachedTokenRegulator); + } + } + + FHE.allow(transferred, cachedRegulator); + if (cachedTokenRegulator != address(0)) { + FHE.allow(transferred, cachedTokenRegulator); + } + + emit TransferInfo(from, to, transferred, $._nextTxId); + + _incrementNextTxId(); + + return transferred; + } + + function supportsInterface(bytes4 interfaceId) public view override(AccessControlDefaultAdminRulesUpgradeable, ERC7984Upgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/wrapper/Wrapper.sol b/protocol-contracts/confidential-wrapper/contracts/wrapper/Wrapper.sol new file mode 100644 index 0000000000..ce19c1986a --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/wrapper/Wrapper.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {FHESafeMath} from "openzeppelin-confidential-contracts/contracts/utils/FHESafeMath.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {FHE, ebool, euint64 } from "@fhevm/solidity/lib/FHE.sol"; +import {AdminProvider} from "../admin/AdminProvider.sol"; +import {FeeManager} from "../admin/FeeManager.sol"; +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + + +/// @notice Wrapper contract for a single token type, providing wrapping/unwrapping functionality +/// @dev Each wrapper handles exactly one underlying token (ERC20 or ETH) and one confidential token +/// @custom:security-contact contact@zaiffer.org +contract Wrapper is ZamaEthereumConfig { + using SafeERC20 for IERC20; + + /// @notice Address of the original token (address(0) for ETH) + address public immutable originalToken; + + /// @notice The paired confidential token + RegulatedERC7984Upgradeable public immutable confidentialToken; + + /// @notice AdminProvider for shared configuration + AdminProvider public immutable adminProvider; + + struct ReceiverEntry { + address to; + bytes callbackData; + } + + uint256 public requestId = 0; + + mapping(uint256 decryptionRequest => ReceiverEntry receiverEntry) private _receivers; + + error IncorrectEthAmount(); + error CannotReceiveEthForTokenWrap(); + error CannotSendToZeroAddress(); + error EthFeeTransferFailed(); + + event Wrapped(uint64 mintAmount, uint256 amountIn, uint256 feeAmount, address indexed to_, uint256 indexed mintTxId); + event UnwrappedFinalized( + uint256 indexed requestId, + bool finalizeSuccess, + bool feeTransferSuccess, + uint64 burnAmount, + uint256 unwrapAmount, + uint256 feeAmount, + uint256 indexed nextTxId + ); + event UnwrappedStarted( + bool returnVal, + uint256 indexed requestId, + uint256 indexed txId, + address indexed to, + euint64 requestedAmount, + euint64 burnAmount + ); + /// @dev The given gateway request ID `requestId` is invalid. + error ERC7984InvalidGatewayRequest(uint256 requestId); + /// @dev FHE.isSenderAllowed(encryptedValue) returned false + error SenderNotAllowed(); + + constructor( + address originalToken_, + RegulatedERC7984Upgradeable confidentialToken_, + AdminProvider adminProvider_ + ) { + originalToken = originalToken_; + confidentialToken = confidentialToken_; + adminProvider = adminProvider_; + } + + /// @notice Wraps original tokens (ETH or ERC20) into confidential tokens + /// @dev Handles fee-on-transfer tokens by tracking actual balances received + /// @param to_ The recipient address for the wrapped confidential tokens + /// @param amount_ The total amount of original tokens to wrap (including fees) + function wrap(address to_, uint256 amount_) external payable { + // Calculate base protocol fee + uint256 baseFee = _getWrapFee(amount_, to_); + uint256 baseAmount = amount_ - baseFee; + + // Calculate dust from uint256 to uint64 conversion + uint256 rate = confidentialToken.rate(); + uint256 wrapDust = baseAmount % rate; + uint256 transferAmount = baseAmount - wrapDust; // == baseAmount / rate * rate + uint256 totalFee = baseFee + wrapDust; + + uint256 mintTxId = confidentialToken.nextTxId(); + + address feeRecipient = _getFeeRecipient(); + uint256 actualFeeReceived; // Actual amount received by fee recipient (for event) + + uint64 mintAmount; + if (originalToken == address(0)) { + require(msg.value == amount_, IncorrectEthAmount()); + + mintAmount = SafeCast.toUint64(transferAmount / rate); + + // For ETH, the fee recipient receives exactly totalFee or the transaction reverts + (bool ethTransferSuccess, ) = feeRecipient.call{value: totalFee}(""); + if (!ethTransferSuccess) { + revert EthFeeTransferFailed(); + } + actualFeeReceived = totalFee; + } else { + require(msg.value == 0, CannotReceiveEthForTokenWrap()); + + // Track wrapper balance to handle fee-on-transfer tokens + uint256 balanceBefore = IERC20(originalToken).balanceOf(address(this)); + IERC20(originalToken).safeTransferFrom(msg.sender, address(this), transferAmount); + uint256 balanceAfter = IERC20(originalToken).balanceOf(address(this)); + + // Track fee recipient balance to emit accurate fee amount + uint256 feeRecipientBalanceBefore = IERC20(originalToken).balanceOf(feeRecipient); + + // Transfer protocol fee to fee recipient + IERC20(originalToken).safeTransferFrom(msg.sender, feeRecipient, totalFee); + + // Calculate actual amount received by wrapper (handles fee-on-transfer tokens) + uint256 balanceDifference = balanceAfter - balanceBefore; + mintAmount = SafeCast.toUint64(balanceDifference / rate); + + // Transfer dust (remainder from uint256→uint64 conversion) to fee recipient + uint256 transferDust = balanceDifference % rate; + IERC20(originalToken).safeTransfer(feeRecipient, transferDust); + + // Calculate actual fee received by fee recipient (for accurate event emission) + uint256 feeRecipientBalanceAfter = IERC20(originalToken).balanceOf(feeRecipient); + actualFeeReceived = feeRecipientBalanceAfter - feeRecipientBalanceBefore; + } + + confidentialToken.mint(to_, mintAmount); + + // Emit event with actual fee received by protocol (not the amount sent) + emit Wrapped(mintAmount, amount_, actualFeeReceived, to_, mintTxId); + } + + /// @notice Initiates unwrapping of confidential tokens back to original tokens (ETH or ERC20) + /// @dev This is the ERC7984 receiver callback, triggered when confidential tokens are transferred to this wrapper + /// @dev Security: Only accepts calls from the paired confidential token contract + /// @dev The unwrap flow is asynchronous: this function burns tokens and requests decryption, then finalizeUnwrap completes the transfer + /// @param from The address that initiated the confidential transfer (token holder) + /// @param amount The encrypted amount of confidential tokens to unwrap + /// @param data ABI-encoded (address to, bytes callbackData) where: + /// - to: recipient address for the unwrapped original tokens + /// - callbackData: optional data passed to IWrapperReceiver.onUnwrapFinalizedReceived if recipient is a contract + /// @return ebool(true) if unwrap was accepted and initiated, ebool(false) if rejected (wrong caller) + function onConfidentialTransferReceived( + address /* operator */, + address from, + euint64 amount, + bytes calldata data + ) external returns (ebool) { + require(FHE.isSenderAllowed(amount), SenderNotAllowed()); + + // burn tx if success is true + // reimpbursement tx if success is false + uint256 nextTxId = confidentialToken.nextTxId(); + + (address to, bytes memory unwrapCallbackData) = abi.decode(data, (address, bytes)); + + bool returnVal = true; + ebool eReturnVal = FHE.asEbool(returnVal); + FHE.allowTransient(eReturnVal, msg.sender); + if (msg.sender != address(confidentialToken)) { + returnVal = false; + eReturnVal = FHE.asEbool(returnVal); + FHE.allowTransient(eReturnVal, msg.sender); + emit UnwrappedStarted(returnVal, 0, nextTxId, to, amount, FHE.asEuint64(0)); + return eReturnVal; + } + + require(to != address(0), CannotSendToZeroAddress()); + + euint64 actualBurnAmount = confidentialToken.burn(amount, from); + + FHE.makePubliclyDecryptable(amount); + FHE.makePubliclyDecryptable(actualBurnAmount); + + _receivers[requestId] = ReceiverEntry({ + to: to, + callbackData: unwrapCallbackData + }); + + emit UnwrappedStarted(returnVal, requestId, nextTxId, to, amount, actualBurnAmount); + + requestId++; + return eReturnVal; + } + + /// @notice Completes the unwrap process using publicly decrypted values + /// @dev This function uses the public decrypt flow where any user (typically the unwrapper) + /// retrieves encrypted handles from the UnwrappedStarted event, decrypts them publicly, + /// and calls this function with the decrypted values and proof. + /// @param requestId The unique identifier for this unwrap request (from UnwrappedStarted event) + /// @param handles Array of encrypted handles [expectedBurnAmount, actualBurnAmount] to verify + /// @param clearTextsAndProof Array containing [abiEncodedClearValues, decryptionProof] from public decryption + /// + /// @dev Success path (actualBurnAmount > 0 && expectedBurnAmount == actualBurnAmount): + /// - Calculates fees in original token units (feeAmount64 * rate) + /// - Transfers fee to fee recipient (if fails, user receives fee + unwrap amount) + /// - Transfers unwrap amount to receiver (if fails, mints back cTokens) + /// - Calls onUnwrapFinalizedReceived if receiver is a contract + /// + /// @dev Failure path (actualBurnAmount == 0 || expectedBurnAmount != actualBurnAmount): + /// - Occurs when user attempted to unwrap 0 or burn more than their balance + /// - No tokens are transferred, emits failure event + /// + /// @dev Parity maintenance: + /// - Both transfers fail: Mints back full actualBurnAmount to maintain parity + /// - Unwrap fails, fee succeeds: Mints back principal only (protocol keeps fees) + /// - Fee fails, unwrap succeeds: User receives unwrapAmount + feeAmount (protocol takes hit) + function finalizeUnwrap( + uint256 requestId, + euint64[] calldata handles, + //euint64 expectedBurnAmountHandle, + //euint64 actualBurnAmountHandle, + bytes[] memory clearTextsAndProof + //bytes memory cleartexts, + //bytes memory decryptionProof + ) external virtual { + { + bytes32[] memory cts = new bytes32[](2); + cts[0] = euint64.unwrap(handles[0]); + cts[1] = euint64.unwrap(handles[1]); + FHE.checkSignatures(cts, clearTextsAndProof[0], clearTextsAndProof[1]); + } + + ( + uint64 expectedBurnAmount, + uint64 actualBurnAmount + ) = abi.decode(clearTextsAndProof[0], (uint64, uint64)); + + ReceiverEntry memory receiver = _receivers[requestId]; + require(receiver.to != address(0), ERC7984InvalidGatewayRequest(requestId)); + + delete _receivers[requestId]; + + uint256 nextTxId = confidentialToken.nextTxId(); + + if (actualBurnAmount > 0 && expectedBurnAmount == actualBurnAmount) { + uint256 rate = confidentialToken.rate(); + uint64 feeAmount64 = _getUnwrapFee(actualBurnAmount, receiver.to); + uint256 feeAmount256 = feeAmount64 * rate; + uint256 unwrapAmount = actualBurnAmount * rate - feeAmount256; + address feeRecipient = _getFeeRecipient(); + + // Transfer fee to fee recipient + bool feeSuccess = _transferUnderlying(originalToken, feeRecipient, feeAmount256); + + if (feeSuccess == false) { + // if fees failed, protocol takes the hit and fees are transferred to user + // on top of unwrap amount to maintain backing token parity + unwrapAmount += feeAmount256; + feeAmount256 = 0; + } + + // Transfer principal to receiver + bool unwrapSuccess = _transferUnderlying(originalToken, receiver.to, unwrapAmount); + + if (unwrapSuccess == false) { + unwrapAmount = 0; + if (feeSuccess == false) { + // Mint everything back to user if both transfers failed + confidentialToken.mint(receiver.to, actualBurnAmount); + feeAmount256 = 0; + } else { + // Mint principal back to user, protocol keeps fees, we'll handle + // this offchain by paying back the user if need be. + // Indeed, either this is a genuine problem it'll be settled offchain + // or the receiver does not accept tokens and the protocol fee should still be paid. + // This ensures token parity is always maintained. + // Note that should the receiver accept tokens, this should never occur. + confidentialToken.mint(receiver.to, actualBurnAmount - feeAmount64); + } + } + emit UnwrappedFinalized(requestId, unwrapSuccess, feeSuccess, actualBurnAmount, unwrapAmount, feeAmount256, nextTxId); + + // solves stack too deep error. + uint256 reqId = requestId; + + if (receiver.to.code.length > 0) { + IWrapperReceiver(receiver.to).onUnwrapFinalizedReceived(msg.sender, unwrapAmount, reqId, msg.sender, receiver.callbackData); + } + } else { + if (actualBurnAmount > 0) { + confidentialToken.mint(receiver.to, actualBurnAmount); + } + emit UnwrappedFinalized(requestId, false, false, actualBurnAmount, 0, 0, nextTxId); + } + } + + function _getWrapFee(uint256 amount_, address to) private view returns (uint256) { + FeeManager feeManager = adminProvider.feeManager(); + return feeManager.getWrapFee(amount_, msg.sender, to); + } + + function _getUnwrapFee(uint64 amount_, address to) private view returns (uint64) { + FeeManager feeManager = adminProvider.feeManager(); + return feeManager.getUnwrapFee(amount_, msg.sender, to); + } + + function _getFeeRecipient() private view returns (address) { + FeeManager feeManager = adminProvider.feeManager(); + return feeManager.getFeeRecipient(); + } + + /// @notice Internal helper to transfer underlying tokens (ETH or ERC20) + /// @dev Abstracts the difference between ETH and ERC20 transfers + /// @dev Uses trySafeTransfer for ERC20 to handle tokens that return false instead of reverting + /// @param token The token address (address(0) for ETH) + /// @param to The recipient address + /// @param amount The amount to transfer + /// @return success True if transfer succeeded, false otherwise + function _transferUnderlying(address token, address to, uint256 amount) internal returns (bool success) { + if (token == address(0)) { + // ETH transfer + (success, ) = to.call{value: amount}(""); + } else { + // ERC20 transfer using trySafeTransfer + success = IERC20(token).trySafeTransfer(to, amount); + } + } +} diff --git a/protocol-contracts/confidential-wrapper/contracts/wrapper/WrapperUpgradeable.sol b/protocol-contracts/confidential-wrapper/contracts/wrapper/WrapperUpgradeable.sol new file mode 100644 index 0000000000..f557ca0aeb --- /dev/null +++ b/protocol-contracts/confidential-wrapper/contracts/wrapper/WrapperUpgradeable.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity 0.8.27; + +import {FHESafeMath} from "openzeppelin-confidential-contracts/contracts/utils/FHESafeMath.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {RegulatedERC7984Upgradeable} from "../token/RegulatedERC7984Upgradeable.sol"; +import {EthereumConfigUpgradeable} from "../fhevm/EthereumConfigUpgradeable.sol"; +import {FHE, ebool, euint64 } from "@fhevm/solidity/lib/FHE.sol"; +import {AdminProvider} from "../admin/AdminProvider.sol"; +import {FeeManager} from "../admin/FeeManager.sol"; +import {IWrapperReceiver} from "../interfaces/IWrapperReceiver.sol"; +import {IDeploymentCoordinator} from "../interfaces/IDeploymentCoordinator.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + + +/// @notice Wrapper contract for a single token type, providing wrapping/unwrapping functionality +/// @dev Each wrapper handles exactly one underlying token (ERC20 or ETH) and one confidential token +/// @custom:security-contact contact@zaiffer.org +contract WrapperUpgradeable is EthereumConfigUpgradeable, AccessControlDefaultAdminRulesUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /// @custom:storage-location erc7201:zaiffer.storage.Wrapper + struct WrapperStorage { + address _originalToken; + RegulatedERC7984Upgradeable _confidentialToken; + IDeploymentCoordinator _deploymentCoordinator; + uint256 _requestId; + mapping(uint256 decryptionRequest => ReceiverEntry receiverEntry) _receivers; + uint64 _mintedSupply; + mapping(address holder => mapping(address operator => uint48 validUntilTimestamp)) _finalizeUnwrapOperators; + } + + struct ReceiverEntry { + address to; + address refund; + bytes callbackData; + euint64 expectedBurnAmount; + euint64 actualBurnAmount; + uint64 committedFeeBasisPoints; + address from; + } + + struct FinalizeSuccessParams { + uint256 requestId; + uint64 actualBurnAmount; + ReceiverEntry receiver; + } + + // keccak256(abi.encode(uint256(keccak256("zaiffer.storage.Wrapper")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant WrapperStorageLocation = + 0x13479f93871f24bad5cbd972b5250a5ad213f4c22829c24117e4b54f246a4500; + + function _getWrapperStorage() internal pure returns (WrapperStorage storage $) { + assembly { + $.slot := WrapperStorageLocation + } + } + + error IncorrectEthAmount(); + error CannotReceiveEthForTokenWrap(); + error CannotSendToZeroAddress(); + error EthFeeTransferFailed(); + error ZeroAddressConfidentialToken(); + error ZeroAddressDeploymentCoordinator(); + error WrapperBalanceExceedsMaxSupply(); + + event Wrapped(uint64 mintAmount, uint256 amountIn, uint256 feeAmount, address indexed to_, uint256 indexed mintTxId); + event UnwrappedFinalized( + uint256 indexed requestId, + bool finalizeSuccess, + bool feeTransferSuccess, + uint64 burnAmount, + uint256 unwrapAmount, + uint256 feeAmount, + uint256 indexed nextTxId + ); + event UnwrappedStarted( + bool returnVal, + uint256 indexed requestId, + uint256 indexed txId, + address indexed to, + address refund, + euint64 requestedAmount, + euint64 burnAmount + ); + event FinalizeUnwrapOperatorSet(address indexed holder, address indexed operator, uint48 until); + /// @dev The given gateway request ID `requestId` is invalid. + error ERC7984InvalidGatewayRequest(uint256 requestId); + /// @dev FHE.isSenderAllowed(encryptedValue) returned false + error SenderNotAllowed(); + /// @dev The caller is not authorized to finalize this unwrap request + error UnauthorizedFinalizeUnwrapCaller(uint256 requestId, address caller, address unwrapInitiator); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address originalToken_, + RegulatedERC7984Upgradeable confidentialToken_, + IDeploymentCoordinator deploymentCoordinator_, + address admin_ + ) public initializer { + require(address(confidentialToken_) != address(0), ZeroAddressConfidentialToken()); + require(address(deploymentCoordinator_) != address(0), ZeroAddressDeploymentCoordinator()); + + __EthereumConfig_init(); + __AccessControlDefaultAdminRules_init(0, admin_); // 0 delay for admin transfer + __ReentrancyGuard_init(); + + WrapperStorage storage $ = _getWrapperStorage(); + $._originalToken = originalToken_; + $._confidentialToken = confidentialToken_; + $._deploymentCoordinator = deploymentCoordinator_; + $._requestId = 0; + } + + function originalToken() public view returns (address) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._originalToken; + } + + function confidentialToken() public view returns (RegulatedERC7984Upgradeable) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._confidentialToken; + } + + function deploymentCoordinator() public view returns (IDeploymentCoordinator) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._deploymentCoordinator; + } + + function adminProvider() public view returns (AdminProvider) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._deploymentCoordinator.adminProvider(); + } + + function requestId() public view returns (uint256) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._requestId; + } + + function getReceiverEntry(uint256 requestId_) public view returns (ReceiverEntry memory) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._receivers[requestId_]; + } + + function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {} + + /// @notice Wraps original tokens (ETH or ERC20) into confidential tokens + /// @dev Handles fee-on-transfer tokens by tracking actual balances received + /// @dev Protected against reentrancy attacks from ERC777 and other callback-enabled tokens + /// @param to_ The recipient address for the wrapped confidential tokens + /// @param amount_ The total amount of original tokens to wrap (including fees) + function wrap(address to_, uint256 amount_) external payable nonReentrant { + WrapperStorage storage $ = _getWrapperStorage(); + + uint256 mintTxId = $._confidentialToken.nextTxId(); + uint64 mintAmount; + uint256 actualFeeReceived; + + uint256 baseFee = _getWrapFee(amount_, to_); + uint256 rate = $._confidentialToken.rate(); + uint256 baseAmount = amount_ - baseFee; + uint256 wrapDust = baseAmount % rate; + uint256 transferAmount = baseAmount - wrapDust; // == baseAmount / rate * rate + uint256 totalFee = amount_ - transferAmount; + + if ($._originalToken == address(0)) { + require(msg.value == amount_, IncorrectEthAmount()); + (mintAmount, actualFeeReceived) = _processETHDeposit(transferAmount, totalFee); + } else { + (mintAmount, actualFeeReceived) = _processERC20Deposit(transferAmount, totalFee); + } + + _mint(to_, mintAmount); + emit Wrapped(mintAmount, amount_, actualFeeReceived, to_, mintTxId); + } + + function _processETHDeposit(uint256 transferAmount_, uint256 totalFee_) private returns (uint64 mintAmount, uint256 actualFeeReceived) { + WrapperStorage storage $ = _getWrapperStorage(); + uint256 rate = $._confidentialToken.rate(); + + mintAmount = SafeCast.toUint64(transferAmount_ / rate); + + address feeRecipient = _getFeeRecipient(); + (bool ethTransferSuccess, ) = feeRecipient.call{value: totalFee_}(""); + if (!ethTransferSuccess) { + revert EthFeeTransferFailed(); + } + + actualFeeReceived = totalFee_; + } + + function _processERC20Deposit(uint256 transferAmount_, uint256 totalFee_) private returns (uint64 mintAmount, uint256 actualFeeReceived) { + require(msg.value == 0, CannotReceiveEthForTokenWrap()); + + WrapperStorage storage $ = _getWrapperStorage(); + uint256 rate = $._confidentialToken.rate(); + address feeRecipient = _getFeeRecipient(); + + // Transfer and track wrapper balance + uint256 balanceBefore = IERC20($._originalToken).balanceOf(address(this)); + IERC20($._originalToken).safeTransferFrom(msg.sender, address(this), transferAmount_); + uint256 balanceDifference = IERC20($._originalToken).balanceOf(address(this)) - balanceBefore; + + mintAmount = SafeCast.toUint64(balanceDifference / rate); + + // Track fee recipient balance to emit accurate fee amount + uint256 feeBalBefore = IERC20($._originalToken).balanceOf(feeRecipient); + + // Transfer fee and track actual received + IERC20($._originalToken).safeTransferFrom(msg.sender, feeRecipient, totalFee_); + + // Transfer dust + uint256 transferDust = balanceDifference % rate; + if (transferDust > 0) { + IERC20($._originalToken).safeTransfer(feeRecipient, transferDust); + } + + actualFeeReceived = IERC20($._originalToken).balanceOf(feeRecipient) - feeBalBefore; + } + + /// @notice Initiates unwrapping of confidential tokens back to original tokens (ETH or ERC20) + /// @dev This is the ERC7984 receiver callback, triggered when confidential tokens are transferred to this wrapper + /// @dev Security: Only accepts calls from the paired confidential token contract + /// @dev The unwrap flow is asynchronous: this function burns tokens and requests decryption, then finalizeUnwrap completes the transfer + /// @param from The address that initiated the confidential transfer (token holder) + /// @param amount The encrypted amount of confidential tokens to unwrap + /// @param data ABI-encoded (address to, address refund, bytes callbackData) where: + /// - to: recipient address for the unwrapped original tokens (may be a contract like SwapV0) + /// - refund: recipient address for refunded cTokens if unwrap fails (typically the user's address) + /// - callbackData: optional data passed to IWrapperReceiver.onUnwrapFinalizedReceived if recipient is a contract + /// @return ebool(true) if unwrap was accepted and initiated, ebool(false) if rejected (wrong caller) + /// + /// @dev Confidentiality + /// - The unwrap amounts are publically decryptable. This is by design since those could be inferred from + /// transferred underlyings at unwrap anyway. + function onConfidentialTransferReceived( + address /* operator */, + address from, + euint64 amount, + bytes calldata data + ) external returns (ebool) { + WrapperStorage storage $ = _getWrapperStorage(); + + require(FHE.isSenderAllowed(amount), SenderNotAllowed()); + + (address to, address refund, bytes memory unwrapCallbackData) = abi.decode(data, (address, address, bytes)); + + ebool eReturnVal = FHE.asEbool(true); + FHE.allowTransient(eReturnVal, msg.sender); + if (msg.sender != address($._confidentialToken)) { + eReturnVal = FHE.asEbool(false); + FHE.allowTransient(eReturnVal, msg.sender); + emit UnwrappedStarted(false, 0, $._confidentialToken.nextTxId(), to, refund, amount, FHE.asEuint64(0)); + return eReturnVal; + } + + require(to != address(0), CannotSendToZeroAddress()); + require(refund != address(0), CannotSendToZeroAddress()); + + _processUnwrap(from, to, refund, amount, unwrapCallbackData); + + return eReturnVal; + } + + function _processUnwrap( + address from, + address to, + address refund, + euint64 amount, + bytes memory unwrapCallbackData + ) private { + WrapperStorage storage $ = _getWrapperStorage(); + + uint256 txId = $._confidentialToken.nextTxId(); + euint64 actualBurnAmount = $._confidentialToken.burn(amount, from); + + FHE.makePubliclyDecryptable(amount); + FHE.makePubliclyDecryptable(actualBurnAmount); + + uint256 requestId = $._requestId; + $._receivers[requestId] = ReceiverEntry({ + to: to, + refund: refund, + callbackData: unwrapCallbackData, + expectedBurnAmount: amount, + actualBurnAmount: actualBurnAmount, + committedFeeBasisPoints: _getUnwrapFeeBasisPoints(to), + from: from + }); + + emit UnwrappedStarted(true, requestId, txId, to, refund, amount, actualBurnAmount); + + $._requestId++; + } + + /// @notice Completes the unwrap process using publicly decrypted values + /// @dev This function uses the public decrypt flow where any user (typically the unwrapper) + /// retrieves encrypted handles from the UnwrappedStarted event, decrypts them publicly, + /// and calls this function with the decrypted values and proof. + /// @param requestId The unique identifier for this unwrap request (from UnwrappedStarted event) + /// @param abiEncodedClearBurnAmounts The ABI-encoded clear values (uint64, uint64) associated to the `decryptionProof`. + /// @param decryptionProof The proof that validates the decryption. + /// + /// @dev Success path (actualBurnAmount > 0 && expectedBurnAmount == actualBurnAmount): + /// - Calculates fees in original token units (feeAmount64 * rate) + /// - Transfers fee to fee recipient (if fails, user receives fee + unwrap amount) + /// - Transfers unwrap amount to receiver (if fails, mints back cTokens) + /// - Calls onUnwrapFinalizedReceived if receiver is a contract + /// + /// @dev Failure path (actualBurnAmount == 0 || expectedBurnAmount != actualBurnAmount): + /// - Occurs when user attempted to unwrap 0 or burn more than their balance + /// - No tokens are transferred, emits failure event + /// + /// @dev Parity maintenance: + /// - Both transfers fail: Mints back full actualBurnAmount to maintain parity + /// - Unwrap fails, fee succeeds: Mints back principal only (protocol keeps fees) + /// - Fee fails, unwrap succeeds: User receives unwrapAmount + feeAmount (protocol takes hit) + function finalizeUnwrap( + uint256 requestId, + bytes memory abiEncodedClearBurnAmounts, + bytes memory decryptionProof + ) external virtual returns (bool) { + WrapperStorage storage $ = _getWrapperStorage(); + + ReceiverEntry memory receiver = $._receivers[requestId]; + require(receiver.to != address(0), ERC7984InvalidGatewayRequest(requestId)); + + // Permission check - only unwrap initiator or authorized operator can finalize + require( + isFinalizeUnwrapOperator(receiver.from, msg.sender), + UnauthorizedFinalizeUnwrapCaller(requestId, msg.sender, receiver.from) + ); + + bytes32[] memory cts = new bytes32[](2); + cts[0] = FHE.toBytes32(receiver.expectedBurnAmount); + cts[1] = FHE.toBytes32(receiver.actualBurnAmount); + + FHE.checkSignatures(cts, abiEncodedClearBurnAmounts, decryptionProof); + + (uint64 expectedBurnAmount, uint64 actualBurnAmount) = abi.decode( + abiEncodedClearBurnAmounts, + (uint64, uint64) + ); + + delete $._receivers[requestId]; + + // Decrement burned supply in onConfidentialTransferReceived + $._mintedSupply -= actualBurnAmount; + + if (actualBurnAmount > 0 && expectedBurnAmount == actualBurnAmount) { + return _finalizeSuccessfulUnwrap(FinalizeSuccessParams(requestId, actualBurnAmount, receiver)); + } else { + _finalizeFailedUnwrap(requestId, actualBurnAmount, receiver); + return false; + } + } + + function _finalizeSuccessfulUnwrap( + FinalizeSuccessParams memory params + ) private returns (bool) { + WrapperStorage storage $ = _getWrapperStorage(); + + uint256 rate = $._confidentialToken.rate(); + uint64 feeAmount64 = _getUnwrapFee(params.actualBurnAmount, params.receiver.committedFeeBasisPoints); + uint256 feeAmount256 = feeAmount64 * rate; + uint256 unwrapAmount = params.actualBurnAmount * rate - feeAmount256; + address feeRecipient = _getFeeRecipient(); + + // Transfer fee to fee recipient + bool feeSuccess = _transferUnderlying($._originalToken, feeRecipient, feeAmount256); + + if (feeSuccess == false) { + // if fees failed, protocol takes the hit and fees are transferred to user + // on top of unwrap amount to maintain backing token parity + unwrapAmount += feeAmount256; + feeAmount256 = 0; + } + + // Transfer principal to receiver + bool unwrapSuccess = _transferUnderlying($._originalToken, params.receiver.to, unwrapAmount); + + // Reimbursement txId if unwrapSuccess is false + uint256 mintTxId = $._confidentialToken.nextTxId(); + + if (unwrapSuccess == false) { + unwrapAmount = 0; + if (feeSuccess == false) { + // Mint everything back to user if both transfers failed + _mint(params.receiver.refund, params.actualBurnAmount); + feeAmount256 = 0; + } else { + // Mint principal back to user, protocol keeps fees, we'll handle + // this offchain by paying back the user if need be. + // Indeed, either this is a genuine problem it'll be settled offchain + // or the receiver does not accept tokens and the protocol fee should still be paid. + // This ensures token parity is always maintained. + // Note that should the receiver accept tokens, this should never occur. + uint64 reimbursementAmount = params.actualBurnAmount - feeAmount64; + _mint(params.receiver.refund, reimbursementAmount); + } + } + + emit UnwrappedFinalized( + params.requestId, + unwrapSuccess, + feeSuccess, + params.actualBurnAmount, + unwrapAmount, + feeAmount256, + mintTxId + ); + + return _executeWrapperReceiverCallback(params.receiver, unwrapAmount, params.requestId); + } + + function _executeWrapperReceiverCallback(ReceiverEntry memory receiver, uint256 unwrapAmount, uint256 requestId) internal returns (bool) { + if (receiver.to.code.length > 0 && receiver.callbackData.length > 0) { + return IWrapperReceiver(receiver.to).onUnwrapFinalizedReceived(msg.sender, unwrapAmount, requestId, receiver.refund, receiver.callbackData); + } + return true; + } + + function _finalizeFailedUnwrap( + uint256 requestId, + uint64 actualBurnAmount, + ReceiverEntry memory receiver + ) private { + WrapperStorage storage $ = _getWrapperStorage(); + + // Reimbursement txId if actualBurnAmount > 0 + uint256 mintTxId = $._confidentialToken.nextTxId(); + + if (actualBurnAmount > 0) { + _mint(receiver.to, actualBurnAmount); + } + emit UnwrappedFinalized(requestId, false, false, actualBurnAmount, 0, 0, mintTxId); + } + + function _mint(address to_, uint64 amount_) private { + WrapperStorage storage $ = _getWrapperStorage(); + + // Safety check: Verify minted supply won't overflow euint64 + require(uint256($._mintedSupply) + uint256(amount_) <= type(uint64).max, WrapperBalanceExceedsMaxSupply()); + + $._confidentialToken.mint(to_, amount_); + $._mintedSupply += amount_; + } + + function mintedSupply() public view returns (uint64) { + WrapperStorage storage $ = _getWrapperStorage(); + return $._mintedSupply; + } + + /// @notice Checks if an address is authorized to finalize unwraps on behalf of a holder + /// @param holder The address that initiated unwraps + /// @param operator The address to check operator status for + /// @return True if operator is authorized (either is the holder or has valid operator permission) + function isFinalizeUnwrapOperator(address holder, address operator) public view virtual returns (bool) { + WrapperStorage storage $ = _getWrapperStorage(); + return holder == operator || block.timestamp <= $._finalizeUnwrapOperators[holder][operator]; + } + + /// @notice Sets an operator that can finalize unwraps on behalf of msg.sender + /// @param operator The address to grant operator permissions to + /// @param until Timestamp until which the operator permission is valid (uint48) + function setFinalizeUnwrapOperator(address operator, uint48 until) public virtual { + _setFinalizeUnwrapOperator(msg.sender, operator, until); + } + + /// @dev Internal function to set operator with event emission + function _setFinalizeUnwrapOperator(address holder, address operator, uint48 until) internal virtual { + WrapperStorage storage $ = _getWrapperStorage(); + $._finalizeUnwrapOperators[holder][operator] = until; + emit FinalizeUnwrapOperatorSet(holder, operator, until); + } + + function _getWrapFee(uint256 amount_, address to) private view returns (uint256) { + FeeManager feeManager = adminProvider().feeManager(); + return feeManager.getWrapFee(amount_, msg.sender, to); + } + + function _getUnwrapFeeBasisPoints(address to) private view returns (uint64) { + FeeManager feeManager = adminProvider().feeManager(); + return feeManager.getUnwrapFeeBasisPoints(msg.sender, to); + } + + function _getUnwrapFee(uint64 amount_, uint64 basisPoints) private view returns (uint64) { + FeeManager feeManager = adminProvider().feeManager(); + return feeManager.getFee(amount_, basisPoints); + } + + function _getFeeRecipient() private view returns (address) { + FeeManager feeManager = adminProvider().feeManager(); + return feeManager.getFeeRecipient(); + } + + /// @notice Internal helper to transfer underlying tokens (ETH or ERC20) + /// @dev Abstracts the difference between ETH and ERC20 transfers + /// @dev Uses trySafeTransfer for ERC20 to handle tokens that return false instead of reverting + /// @param token The token address (address(0) for ETH) + /// @param to The recipient address + /// @param amount The amount to transfer + /// @return success True if transfer succeeded, false otherwise + function _transferUnderlying(address token, address to, uint256 amount) internal returns (bool success) { + if (token == address(0)) { + // ETH transfer + (success, ) = to.call{value: amount}(""); + } else { + // ERC20 transfer using trySafeTransfer + success = IERC20(token).trySafeTransfer(to, amount); + } + } +} diff --git a/protocol-contracts/confidential-wrapper/deploy/deploy.ts b/protocol-contracts/confidential-wrapper/deploy/deploy.ts new file mode 100644 index 0000000000..01f19243c6 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/deploy/deploy.ts @@ -0,0 +1,51 @@ +import { DeployFunction } from "hardhat-deploy/types"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployer } = await hre.getNamedAccounts(); + const { deploy } = hre.deployments; + + // Deploy SanctionsList first + const deployedSanctionsList = await deploy("SanctionsList", { + from: deployer, + log: true, + }); + + // Deploy FeeManager + const deployedFeeManager = await deploy("FeeManager", { + from: deployer, + log: true, + }); + + // Deploy AdminProvider with SanctionsList and FeeManager + const deployedAdminProvider = await deploy("AdminProvider", { + from: deployer, + args: [deployedSanctionsList.address, deployedFeeManager.address], + log: true, + }); + + // Deploy WrapperFactory with AdminProvider + const deployedWrapperFactory = await deploy("WrapperFactory", { + from: deployer, + args: [deployedAdminProvider.address], + log: true, + }); + + // Set up initial fee configuration + const feeManagerContract = await hre.ethers.getContractAt("FeeManager", deployedFeeManager.address); + const deployFee = "1000000000000000000"; // 1 ETH in wei + const feeRecipient = deployer; // Use deployer as fee recipient for now + + await feeManagerContract.setDeployFee(deployFee); + await feeManagerContract.setFeeRecipient(feeRecipient); + + console.log(`SanctionsList contract: `, deployedSanctionsList.address); + console.log(`FeeManager contract: `, deployedFeeManager.address); + console.log(`AdminProvider contract: `, deployedAdminProvider.address); + console.log(`WrapperFactory contract: `, deployedWrapperFactory.address); + console.log(`Deploy fee set to: `, deployFee, " wei"); + console.log(`Fee recipient set to: `, feeRecipient); +}; +export default func; +func.id = "deploy_wrapper_factory"; // id required to prevent reexecution +func.tags = ["WrapperFactory", "AdminProvider", "FeeManager", "SanctionsList"]; diff --git a/protocol-contracts/confidential-wrapper/hardhat.config.ts b/protocol-contracts/confidential-wrapper/hardhat.config.ts new file mode 100644 index 0000000000..e2e4bc1611 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/hardhat.config.ts @@ -0,0 +1,91 @@ +import "@openzeppelin/hardhat-upgrades"; +import "@fhevm/hardhat-plugin"; +import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomicfoundation/hardhat-ethers"; +import "@nomicfoundation/hardhat-verify"; +import "@typechain/hardhat"; +import "hardhat-deploy"; +import "hardhat-gas-reporter"; +import type { HardhatUserConfig } from "hardhat/config"; +import { vars } from "hardhat/config"; +import "solidity-coverage"; + +import "./tasks/accounts"; +import "./tasks/FHECounter"; + +// Run 'npx hardhat vars setup' to see the list of variables that need to be set + +const MNEMONIC: string = vars.get("MNEMONIC", "test test test test test test test test test test test junk"); +const INFURA_API_KEY: string = vars.get("INFURA_API_KEY", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"); + +const config: HardhatUserConfig = { + defaultNetwork: "hardhat", + namedAccounts: { + deployer: 0, + }, + etherscan: { + apiKey: { + sepolia: vars.get("ETHERSCAN_API_KEY", ""), + }, + }, + gasReporter: { + currency: "USD", + enabled: process.env.REPORT_GAS ? true : false, + excludeContracts: [], + }, + networks: { + hardhat: { + accounts: { + mnemonic: MNEMONIC, + }, + chainId: 31337, + }, + anvil: { + accounts: { + mnemonic: MNEMONIC, + path: "m/44'/60'/0'/0/", + count: 10, + }, + chainId: 31337, + url: "http://localhost:8545", + }, + sepolia: { + accounts: { + mnemonic: MNEMONIC, + path: "m/44'/60'/0'/0/", + count: 10, + }, + chainId: 11155111, + url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`, + }, + }, + paths: { + artifacts: "./artifacts", + cache: "./cache", + sources: "./contracts", + tests: "./test", + }, + solidity: { + version: "0.8.27", + settings: { + metadata: { + // Not including the metadata hash + // https://github.com/paulrberg/hardhat-template/issues/31 + bytecodeHash: "none", + }, + // Disable the optimizer when debugging + // https://hardhat.org/hardhat-network/#solidity-optimizer-support + optimizer: { + enabled: true, + runs: 100, + }, + evmVersion: "cancun", + }, + }, + typechain: { + outDir: "types", + target: "ethers-v6", + }, +}; + +export default config; diff --git a/protocol-contracts/confidential-wrapper/img/admin-configuration-flow.png b/protocol-contracts/confidential-wrapper/img/admin-configuration-flow.png new file mode 100644 index 0000000000..191e1cdaed Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/admin-configuration-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/batch-transfer-flow.png b/protocol-contracts/confidential-wrapper/img/batch-transfer-flow.png new file mode 100644 index 0000000000..8fc8c8290e Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/batch-transfer-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/confidential-transfer-flow.png b/protocol-contracts/confidential-wrapper/img/confidential-transfer-flow.png new file mode 100644 index 0000000000..5827421544 Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/confidential-transfer-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/deployment-flow.png b/protocol-contracts/confidential-wrapper/img/deployment-flow.png new file mode 100644 index 0000000000..9089c1b3f2 Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/deployment-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/swap-flow.png b/protocol-contracts/confidential-wrapper/img/swap-flow.png new file mode 100644 index 0000000000..5bea6ac4b9 Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/swap-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/system-deployment-flow.png b/protocol-contracts/confidential-wrapper/img/system-deployment-flow.png new file mode 100644 index 0000000000..bec54fe496 Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/system-deployment-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/token-deployment-flow.png b/protocol-contracts/confidential-wrapper/img/token-deployment-flow.png new file mode 100644 index 0000000000..f2c8f2c82e Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/token-deployment-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/token-unwrapping-flow.png b/protocol-contracts/confidential-wrapper/img/token-unwrapping-flow.png new file mode 100644 index 0000000000..3d39df37cf Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/token-unwrapping-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/img/token-wrapping-flow.png b/protocol-contracts/confidential-wrapper/img/token-wrapping-flow.png new file mode 100644 index 0000000000..1cee86ded5 Binary files /dev/null and b/protocol-contracts/confidential-wrapper/img/token-wrapping-flow.png differ diff --git a/protocol-contracts/confidential-wrapper/package-lock.json b/protocol-contracts/confidential-wrapper/package-lock.json new file mode 100644 index 0000000000..0531292a45 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/package-lock.json @@ -0,0 +1,13025 @@ +{ + "name": "fhevm-hardhat-template", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fhevm-hardhat-template", + "version": "0.1.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@fhevm/solidity": "^0.9.1", + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@zama-fhe/oracle-solidity": "^0.1.0", + "encrypted-types": "^0.0.4", + "express": "^5.1.0", + "openzeppelin-confidential-contracts": "github:RegisGraptin/openzeppelin-confidential-contracts#60e815a49fb694854dfb966584f439e941fa51bb" + }, + "devDependencies": { + "@fhevm/hardhat-plugin": "^0.3.0-1", + "@nomicfoundation/hardhat-chai-matchers": "^2.1.0", + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-network-helpers": "^1.1.0", + "@nomicfoundation/hardhat-verify": "^2.1.0", + "@openzeppelin/hardhat-upgrades": "^3.9.1", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", + "@types/node": "^20.19.8", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "@zama-fhe/relayer-sdk": "^0.3.0-5", + "chai": "^4.5.0", + "chai-as-promised": "^8.0.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "ethers": "^6.15.0", + "hardhat": "^2.26.0", + "hardhat-deploy": "^0.11.45", + "hardhat-gas-reporter": "^2.3.0", + "mocha": "^11.7.1", + "prettier": "^3.6.2", + "prettier-plugin-solidity": "^2.1.0", + "rimraf": "^6.0.1", + "solhint": "^6.0.0", + "solidity-coverage": "^0.8.16", + "ts-generator": "^0.1.1", + "ts-node": "^10.9.2", + "typechain": "^8.3.2", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=20", + "npm": ">=7.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.930.0.tgz", + "integrity": "sha512-Tj92ZTdHUqQj41Sn7T4AVNf7s59ijPItgz83je9l3JqbYfVQ9+M+KpjTCg7o6pygTztkNuLo/WvDP6YlEQgXVg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/credential-provider-node": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/eventstream-serde-browser": "^4.2.5", + "@smithy/eventstream-serde-config-resolver": "^4.3.5", + "@smithy/eventstream-serde-node": "^4.2.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.930.0.tgz", + "integrity": "sha512-sASqgm1iMLcmi+srSH9WJuqaf3GQAKhuB4xIJwkNEPUQ+yGV8HqErOOHJLXXuTUyskcdtK+4uMaBRLT2ESm+QQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.930.0.tgz", + "integrity": "sha512-E95pWT1ayfRWg0AW2KNOCYM7QQcVeOhMRLX5PXLeDKcdxP7s3x0LHG9t7a3nPbAbvYLRrhC7O2lLWzzMCpqjsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.930.0.tgz", + "integrity": "sha512-5tJyxNQmm9C1XKeiWt/K67mUHtTiU2FxTkVsqVrzAMjNsF3uyA02kyTK70byh5n29oVR9XNValVEl6jk01ipYg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.930.0.tgz", + "integrity": "sha512-vw565GctpOPoRJyRvgqXM8U/4RG8wYEPfhe6GHvt9dchebw0OaFeW1mmSYpwEPkMhZs9Z808dkSPScwm8WZBKA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.930.0.tgz", + "integrity": "sha512-Ua4T5MWjm7QdHi7ZSUvnPBFwBZmLFP/IEGCLacPKbUT1sQO30hlWuB/uQOj0ns4T6p7V4XsM8bz5+xsW2yRYbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/credential-provider-env": "3.930.0", + "@aws-sdk/credential-provider-http": "3.930.0", + "@aws-sdk/credential-provider-process": "3.930.0", + "@aws-sdk/credential-provider-sso": "3.930.0", + "@aws-sdk/credential-provider-web-identity": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.930.0.tgz", + "integrity": "sha512-LTx5G0PsL51hNCCzOIdacGPwqnTp3X2Ck8CjLL4Kz9FTR0mfY02qEJB5y5segU1hlge/WdQYxzBBMhtMUR2h8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.930.0", + "@aws-sdk/credential-provider-http": "3.930.0", + "@aws-sdk/credential-provider-ini": "3.930.0", + "@aws-sdk/credential-provider-process": "3.930.0", + "@aws-sdk/credential-provider-sso": "3.930.0", + "@aws-sdk/credential-provider-web-identity": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.930.0.tgz", + "integrity": "sha512-lqC4lepxgwR2uZp/JROTRjkHld4/FEpSgofmiIOAfUfDx0OWSg7nkWMMS/DzlMpODqATl9tO0DcvmIJ8tMbh6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.930.0.tgz", + "integrity": "sha512-LIs2aaVoFfioRokR1R9SpLS9u8CmbHhrV/gpHO1ED41qNCujn23vAxRNQmWzJ2XoCxSTwvToiHD2i6CjPA6rHQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.930.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/token-providers": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.930.0.tgz", + "integrity": "sha512-iIYF8GReLOp16yn2bnRWrc4UOW/vVLifqyRWZ3iAGe8NFzUiHBq+Nok7Edh+2D8zt30QOCOsWCZ31uRrPuXH8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.930.0.tgz", + "integrity": "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.930.0.tgz", + "integrity": "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.930.0.tgz", + "integrity": "sha512-gv0sekNpa2MBsIhm2cjP3nmYSfI4nscx/+K9u9ybrWZBWUIC4kL2sV++bFjjUz4QxUIlvKByow3/a9ARQyCu7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.930.0.tgz", + "integrity": "sha512-UUItqy02biaHoZDd1Z2CskFon3Lej15ZCIZzW4n2lsJmgLWNvz21jtFA8DQny7ZgCLAOOXI8YK3VLZptZWtIcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.930.0.tgz", + "integrity": "sha512-eEDjTVXNiDkoV0ZV+X+WV40GTpF70xZmDW13CQzQF7rzOC2iFjtTRU+F7MUhy/Vs+e9KvDgiuCDecITtaOXUNw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.930.0.tgz", + "integrity": "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.930.0.tgz", + "integrity": "sha512-K+fJFJXA2Tdx10WhhTm+xQmf1WDHu14rUutByyqx6W0iW2rhtl3YeRr188LWSU3/hpz7BPyvigaAb0QyRti6FQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.930.0.tgz", + "integrity": "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.930.0.tgz", + "integrity": "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.930.0.tgz", + "integrity": "sha512-tYc5uFKogn0vLukeZ6Zz2dR1/WiTjxZH7+Jjoce6aEYgRVfyrDje1POFb7YxhNZ7Pp1WzHCuwW2KgkmMoYVbxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.0.tgz", + "integrity": "sha512-JorcEwe4ud0x5BS/Ar2aQWOQoFzjq/7jcnxYXCvSMh0oRm0dQXzOA+hqLDBnOMks1LLBA7dmiLLsEBl09Yd6iQ==", + "dev": true, + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz", + "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-9.1.0.tgz", + "integrity": "sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "ethereum-cryptography": "^2.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fhevm/hardhat-plugin": { + "version": "0.3.0-1", + "resolved": "https://registry.npmjs.org/@fhevm/hardhat-plugin/-/hardhat-plugin-0.3.0-1.tgz", + "integrity": "sha512-SnxtynkDehpkvoq0CRi1x09J01/K1rP2Q99fMNUv6PxXowwvjhZYhq0vw8qi6mZLI62AVMh94KbzN/Gb3L4e/w==", + "dev": true, + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@fhevm/host-contracts": "0.9.0", + "debug": "^4.1.1", + "dotenv": "^16.5.0", + "picocolors": "^1.1.1", + "resolve": "^1.22.10" + }, + "engines": { + "node": ">=20", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@fhevm/mock-utils": "0.3.0-1", + "@fhevm/solidity": "^0.9.1", + "@nomicfoundation/hardhat-ethers": "^3.0.8", + "@zama-fhe/relayer-sdk": "^0.3.0-5", + "encrypted-types": "^0.0.4", + "ethers": "^6.1.0", + "hardhat": "^2.0.0" + } + }, + "node_modules/@fhevm/host-contracts": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@fhevm/host-contracts/-/host-contracts-0.9.0.tgz", + "integrity": "sha512-N/DRfpXVCzHbg+9NE5sLHzTH3X7Wt3xRSN/7Jyoo+mKgjI7u40heJz8z/dUqvcq0h8wXjXcZcQHgP4pjMNjLKw==", + "dev": true, + "license": "BSD-3-Clause-Clear", + "dependencies": { + "encrypted-types": "^0.0.4" + }, + "optionalDependencies": { + "solidity-comments-darwin-arm64": "0.1.1", + "solidity-comments-linux-x64-gnu": "0.1.1" + } + }, + "node_modules/@fhevm/mock-utils": { + "version": "0.3.0-1", + "resolved": "https://registry.npmjs.org/@fhevm/mock-utils/-/mock-utils-0.3.0-1.tgz", + "integrity": "sha512-1mfzU66Y1r3qc8QUlqnOtENPKchmNx2l1WxFWlcI6CzdskUoywvLRz+w0wnmze8GUpWpFgoclpK2okw1LlzgBQ==", + "dev": true, + "license": "BSD-3-Clause-Clear", + "peer": true, + "peerDependencies": { + "@zama-fhe/relayer-sdk": "^0.3.0-5", + "ethers": "^6.1.0", + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@fhevm/solidity": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@fhevm/solidity/-/solidity-0.9.1.tgz", + "integrity": "sha512-QxP+R+6v3aHA/rzgH9cDsiYOwRBB6zLAL3qNI4YfRewKA6ZB01cdkYvrB80FkJyddzxRNsT1jHIvJvSRS8XZ+Q==", + "license": "BSD-3-Clause-Clear", + "peer": true, + "dependencies": { + "encrypted-types": "^0.0.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "solidity-comments-darwin-arm64": "0.1.1", + "solidity-comments-linux-x64-gnu": "0.1.1" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nomicfoundation/edr": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.12.0-next.14.tgz", + "integrity": "sha512-MGHY2x7JaNdkqlQxFBYoM7Miw2EqsQrI3ReVZMwLP5mULSRTAOnt3hCw6cnjXxGi991HnejNAedJofke6OdqqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/edr-darwin-arm64": "0.12.0-next.14", + "@nomicfoundation/edr-darwin-x64": "0.12.0-next.14", + "@nomicfoundation/edr-linux-arm64-gnu": "0.12.0-next.14", + "@nomicfoundation/edr-linux-arm64-musl": "0.12.0-next.14", + "@nomicfoundation/edr-linux-x64-gnu": "0.12.0-next.14", + "@nomicfoundation/edr-linux-x64-musl": "0.12.0-next.14", + "@nomicfoundation/edr-win32-x64-msvc": "0.12.0-next.14" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.14.tgz", + "integrity": "sha512-sl0DibKSUOS7JXhUtaQ6FJUY+nk+uq5gx+Fyd9iiqs8awZPNn6KSuvV1EbWCi+yd3mrxgZ/wO8E77C1Dxj4xQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.14.tgz", + "integrity": "sha512-lfmatc1MSOaw0rDFB+ynnAGz5TWm3hSeY/+zDpPZghMODZelXm4JCqF41CQ6paLsW3X/pXcHM1HUGCUBWeoI/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.14.tgz", + "integrity": "sha512-sWun3PhVgat8d4lg1d5MAXSIsFlSMBzvrpMSDFNOU9hPJEclSHbHBMRcarQuGqwm/5ZBzTwCS25u78A+UATTrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.14.tgz", + "integrity": "sha512-omWKioD8fFp7ayCeSDu2CqvG78+oYw8zdVECDwZVmE0jpszRCsTufNYflWRQnlGqH6GqjEUwq2c3yLxFgOTjFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.14.tgz", + "integrity": "sha512-vk0s4SaC7s1wa98W24a4zqunTK/yIcSEnsSLRM/Nl+JJs6iqS8tvmnh/BbFINORMBJ065OWc10qw2Lsbu/rxtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.14.tgz", + "integrity": "sha512-/xKQD6c2RXQBIb30iTeh/NrMdYvHs6Nd+2UXS6wxlfX7GzRPOkpVDiDGD7Sda82JI459KH67dADOD6CpX8cpHQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { + "version": "0.12.0-next.14", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.14.tgz", + "integrity": "sha512-GZcyGdOoLWnUtfPU+6B1vUi4fwf3bouSRf3xuKFHz3p/WNhpDK+8Esq3UmOmYAZWRgFT0ZR6XUk9H2owGDTVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/hardhat-chai-matchers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.1.0.tgz", + "integrity": "sha512-GPhBNafh1fCnVD9Y7BYvoLnblnvfcq3j8YDbO1gGe/1nOFWzGmV7gFu5DkwFXF+IpYsS+t96o9qc/mPu3V3Vfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai-as-promised": "^7.1.3", + "chai-as-promised": "^7.1.1", + "deep-eql": "^4.0.1", + "ordinal": "^1.0.3" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "chai": "^4.2.0", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-chai-matchers/node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.2.tgz", + "integrity": "sha512-7xEaz2X8p47qWIAqtV2z03MmusheHm8bvY2mDlxo9JiT2BgSx59GSdv5+mzwOvsuKDbTij7oqDnwFyYOlHREEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "lodash.isequal": "^4.5.0" + }, + "peerDependencies": { + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-network-helpers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.1.2.tgz", + "integrity": "sha512-p7HaUVDbLj7ikFivQVNhnfMHUBgiHYMwQWvGn9AriieuopGOELIrwj2KjyM2a6z70zai5YKO264Vwz+3UFJZPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ethereumjs-util": "^7.1.4" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-verify": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.3.tgz", + "integrity": "sha512-danbGjPp2WBhLkJdQy9/ARM3WQIK+7vwzE0urNem1qZJjh9f54Kf5f1xuQv8DvqewUAkuPxVt/7q4Grz5WjqSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^8.1.0", + "debug": "^4.1.1", + "lodash.clonedeep": "^4.5.0", + "picocolors": "^1.1.0", + "semver": "^6.3.0", + "table": "^6.8.0", + "undici": "^5.14.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/slang": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.18.3.tgz", + "integrity": "sha512-YqAWgckqbHM0/CZxi9Nlf4hjk9wUNLC9ngWCWBiqMxPIZmzsVKYuChdlrfeBPQyvQQBoOhbx+7C1005kLVQDZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.0" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", + "integrity": "sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.2", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.2" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz", + "integrity": "sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz", + "integrity": "sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz", + "integrity": "sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz", + "integrity": "sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", + "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", + "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", + "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@openzeppelin/contracts": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz", + "integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==", + "license": "MIT", + "peer": true + }, + "node_modules/@openzeppelin/contracts-upgradeable": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.4.0.tgz", + "integrity": "sha512-STJKyDzUcYuB35Zub1JpWW58JxvrFFVgQ+Ykdr8A9PGXgtq/obF5uoh07k2XmFyPxfnZdPdBdhkJ/n2YxJ87HQ==", + "license": "MIT", + "peerDependencies": { + "@openzeppelin/contracts": "5.4.0" + } + }, + "node_modules/@openzeppelin/defender-sdk-base-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-base-client/-/defender-sdk-base-client-2.7.0.tgz", + "integrity": "sha512-J5IpvbFfdIJM4IadBcXfhCXVdX2yEpaZtRR1ecq87d8CdkmmEpniYfef/yVlG98yekvu125LaIRg0yXQOt9Bdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-lambda": "^3.563.0", + "amazon-cognito-identity-js": "^6.3.6", + "async-retry": "^1.3.3" + } + }, + "node_modules/@openzeppelin/defender-sdk-deploy-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-deploy-client/-/defender-sdk-deploy-client-2.7.0.tgz", + "integrity": "sha512-YOHZmnHmM1y6uSqXWGfk2/5/ae4zZJE6xG92yFEAIOy8vqh1dxznWMsoCcAXRXTCWc8RdCDpFdMfEy4SBTyYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.7.0", + "axios": "^1.7.4", + "lodash": "^4.17.21" + } + }, + "node_modules/@openzeppelin/defender-sdk-network-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-network-client/-/defender-sdk-network-client-2.7.0.tgz", + "integrity": "sha512-4CYWPa9+kSjojE5KS7kRmP161qsBATdp97TCrzyDdGoVahj0GyqgafRL9AAjm0eHZOM1c7EIYEpbvYRtFi8vyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.7.0", + "axios": "^1.7.4", + "lodash": "^4.17.21" + } + }, + "node_modules/@openzeppelin/hardhat-upgrades": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.9.1.tgz", + "integrity": "sha512-pSDjlOnIpP+PqaJVe144dK6VVKZw2v6YQusyt0OOLiCsl+WUzfo4D0kylax7zjrOxqy41EK2ipQeIF4T+cCn2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.1.0", + "@openzeppelin/defender-sdk-deploy-client": "^2.1.0", + "@openzeppelin/defender-sdk-network-client": "^2.1.0", + "@openzeppelin/upgrades-core": "^1.41.0", + "chalk": "^4.1.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.1.5", + "proper-lockfile": "^4.1.1", + "undici": "^6.11.1" + }, + "bin": { + "migrate-oz-cli-project": "dist/scripts/migrate-oz-cli-project.js" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.0.6", + "@nomicfoundation/hardhat-verify": "^2.0.14", + "ethers": "^6.6.0", + "hardhat": "^2.24.1" + }, + "peerDependenciesMeta": { + "@nomicfoundation/hardhat-verify": { + "optional": true + } + } + }, + "node_modules/@openzeppelin/hardhat-upgrades/node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.44.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.44.2.tgz", + "integrity": "sha512-m6iorjyhPK9ow5/trNs7qsBC/SOzJCO51pvvAF2W9nOiZ1t0RtCd+rlRmRmlWTv4M33V0wzIUeamJ2BPbzgUXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/slang": "^0.18.3", + "bignumber.js": "^9.1.2", + "cbor": "^10.0.0", + "chalk": "^4.1.0", + "compare-versions": "^6.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "minimatch": "^9.0.5", + "minimist": "^1.2.7", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.60" + }, + "bin": { + "openzeppelin-upgrades-core": "dist/cli/cli.js" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.11.tgz", + "integrity": "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "nofilter": "^3.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", + "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/tracing/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.3.tgz", + "integrity": "sha512-qqpNskkbHOSfrbFbjhYj5o8VMXO26fvN1K/+HbCzUNlTuxgNcPRouUDNm+7D6CkN244WG7aK533Ne18UtJEgAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", + "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", + "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", + "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", + "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", + "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.10.tgz", + "integrity": "sha512-SoAag3QnWBFoXjwa1jenEThkzJYClidZUyqsLKwWZ8kOlZBwehrLBp4ygVDjNEM2a2AamCQ2FBA/HuzKJ/LiTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.3", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.10.tgz", + "integrity": "sha512-6fOwX34gXxcqKa3bsG0mR0arc2Cw4ddOS6tp3RgUD2yoTrDTbQ2aVADnDjhUuxaiDZN2iilxndgGDhnpL/XvJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.5.tgz", + "integrity": "sha512-La1ldWTJTZ5NqQyPqnCNeH9B+zjFhrNoQIL1jTh4zuqXRlmXhxYHhMtI1/92OlnoAtp6JoN7kzuwhWoXrBwPqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.6.tgz", + "integrity": "sha512-hGz42hggqReicRRZUvrKDQiAmoJnx1Q+XfAJnYAGu544gOfxQCAC3hGGD7+Px2gEUUxB/kKtQV7LOtBRNyxteQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.3", + "@smithy/middleware-endpoint": "^4.3.10", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.9.tgz", + "integrity": "sha512-Bh5bU40BgdkXE2BcaNazhNtEXi1TC0S+1d84vUwv5srWfvbeRNUKFzwKQgC6p6MXPvEgw+9+HdX3pOwT6ut5aw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.12.tgz", + "integrity": "sha512-EHZwe1E9Q7umImIyCKQg/Cm+S+7rjXxCRvfGmKifqwYvn7M8M4ZcowwUOQzvuuxUUmdzCkqL0Eq0z1m74Pq6pw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", + "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typechain/ethers-v6": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.2", + "typescript": ">=4.7.0" + } + }, + "node_modules/@typechain/hardhat": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-9.1.0.tgz", + "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "@typechain/ethers-v6": "^0.5.1", + "ethers": "^6.1.0", + "hardhat": "^2.9.9", + "typechain": "^8.3.2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/secp256k1": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.7.tgz", + "integrity": "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.4", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@uniswap/lib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-1.1.1.tgz", + "integrity": "sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-periphery": { + "version": "1.1.0-beta.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz", + "integrity": "sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@uniswap/lib": "1.1.1", + "@uniswap/v2-core": "1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-periphery/node_modules/@uniswap/v2-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.0.tgz", + "integrity": "sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@zama-fhe/oracle-solidity": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@zama-fhe/oracle-solidity/-/oracle-solidity-0.1.0.tgz", + "integrity": "sha512-phRego2FW7SWgneQOES/iQ99c97ZCb+KZk5m+lT474dSNrsgEDh96W9T1+Owhc9C6VKtCpMLM43dHXwKHDIw6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@openzeppelin/contracts-upgradeable": "^5.0.2" + } + }, + "node_modules/@zama-fhe/relayer-sdk": { + "version": "0.3.0-5", + "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.3.0-5.tgz", + "integrity": "sha512-DIPlN9z5tfSCXqUlhQN2MEv57HmAUHSGV9LPtGs6LWmM7POj6WsMd1hU7Qci4AWPsor7k3IqMfqGGthIvot/DQ==", + "dev": true, + "license": "BSD-3-Clause-Clear", + "peer": true, + "dependencies": { + "commander": "^14.0.0", + "ethers": "^6.15.0", + "fetch-retry": "^6.0.0", + "keccak": "^3.0.4", + "node-tfhe": "1.4.0-alpha.3", + "node-tkms": "^0.12.0", + "tfhe": "1.4.0-alpha.3", + "tkms": "^0.12.0", + "wasm-feature-detect": "^1.8.0" + }, + "bin": { + "relayer": "bin/relayer.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/abitype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", + "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/amazon-cognito-identity-js": { + "version": "6.3.16", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.16.tgz", + "integrity": "sha512-HPGSBGD6Q36t99puWh0LnptxO/4icnk2kqIQ9cTJ2tFQo5NMUnWQIgtrTAk8nm+caqUbjDzXzG56GBjI2tS6jQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "dev": true, + "license": "BSD-3-Clause OR MIT", + "optional": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ast-parents": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", + "integrity": "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==", + "dev": true, + "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/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-ajv-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.2.tgz", + "integrity": "sha512-1cLrJXEq46n0hjV8dDYwg9LKYjDb3KbeW7nZTv4kvfoDD9c2DXHIE31nxM+Y/cIfXMggLUfmxbm6h/JoM/yotA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@humanwhocodes/momoa": "^2.0.4", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0 < 4" + }, + "engines": { + "node": ">= 18.20.6" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/brotli-wasm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brotli-wasm/-/brotli-wasm-2.0.1.tgz", + "integrity": "sha512-+3USgYsC7bzb5yU0/p2HnnynZl0ak0E6uoIm4UW4Aby/8s8HFCq6NCfrrf1E9c3O8OCSzq3oYO1tUVqIi61Nww==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "license": "MIT", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", + "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "check-error": "^2.1.1" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 7" + } + }, + "node_modules/chai-as-promised/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/command-line-usage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-usage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/command-line-usage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true, + "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" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/death": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/death/-/death-1.1.0.tgz", + "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "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" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encrypted-types": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/encrypted-types/-/encrypted-types-0.0.4.tgz", + "integrity": "sha512-f55ccBBUwvqWqr3ymAVOLZ6bzjsSQZlDN0GcKFmzkvTpml4Vm3Y6BCaHhCuW/ctrabTJJ3DFnUsjtFOpokJUaQ==", + "license": "MIT", + "peer": true + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=0.12.0" + }, + "optionalDependencies": { + "source-map": "~0.2.0" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethereum-bloom-filters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.2.0.tgz", + "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.4.0" + } + }, + "node_modules/ethereum-bloom-filters/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", + "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pbkdf2": "^3.0.0", + "@types/secp256k1": "^4.0.1", + "blakejs": "^1.1.0", + "browserify-aes": "^1.2.0", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "hash.js": "^1.1.7", + "keccak": "^3.0.0", + "pbkdf2": "^3.0.17", + "randombytes": "^2.1.0", + "safe-buffer": "^5.1.2", + "scrypt-js": "^3.0.0", + "secp256k1": "^4.0.1", + "setimmediate": "^1.0.5" + } + }, + "node_modules/ethereumjs-util": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ethers": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", + "integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/ethjs-unit/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-retry": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", + "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fmix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", + "integrity": "sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "imul": "^1.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fp-ts": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", + "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ghost-testrpc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz", + "integrity": "sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^2.4.2", + "node-emoji": "^1.10.0" + }, + "bin": { + "testrpc-sc": "index.js" + } + }, + "node_modules/ghost-testrpc/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ghost-testrpc/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ghost-testrpc/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hardhat": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.27.0.tgz", + "integrity": "sha512-du7ecjx1/ueAUjvtZhVkJvWytPCjlagG3ZktYTphfzAbc1Flc6sRolw5mhKL/Loub1EIFRaflutM4bdB/YsUUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/abi": "^5.1.2", + "@nomicfoundation/edr": "^0.12.0-next.7", + "@nomicfoundation/solidity-analyzer": "^0.1.0", + "@sentry/node": "^5.18.1", + "adm-zip": "^0.4.16", + "aggregate-error": "^3.0.0", + "ansi-escapes": "^4.3.0", + "boxen": "^5.1.2", + "chokidar": "^4.0.0", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "enquirer": "^2.3.0", + "env-paths": "^2.2.0", + "ethereum-cryptography": "^1.0.3", + "find-up": "^5.0.0", + "fp-ts": "1.19.3", + "fs-extra": "^7.0.1", + "immutable": "^4.0.0-rc.12", + "io-ts": "1.10.4", + "json-stream-stringify": "^3.1.4", + "keccak": "^3.0.2", + "lodash": "^4.17.11", + "micro-eth-signer": "^0.14.0", + "mnemonist": "^0.38.0", + "mocha": "^10.0.0", + "p-map": "^4.0.0", + "picocolors": "^1.1.0", + "raw-body": "^2.4.1", + "resolve": "1.17.0", + "semver": "^6.3.0", + "solc": "0.8.26", + "source-map-support": "^0.5.13", + "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.6", + "tsort": "0.0.1", + "undici": "^5.14.0", + "uuid": "^8.3.2", + "ws": "^7.4.6" + }, + "bin": { + "hardhat": "internal/cli/bootstrap.js" + }, + "peerDependencies": { + "ts-node": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/hardhat-deploy": { + "version": "0.11.45", + "resolved": "https://registry.npmjs.org/hardhat-deploy/-/hardhat-deploy-0.11.45.tgz", + "integrity": "sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/solidity": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "@types/qs": "^6.9.7", + "axios": "^0.21.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "ethers": "^5.7.0", + "form-data": "^4.0.0", + "fs-extra": "^10.0.0", + "match-all": "^1.2.6", + "murmur-128": "^0.2.1", + "qs": "^6.9.4", + "zksync-web3": "^0.14.3" + } + }, + "node_modules/hardhat-deploy/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/hardhat-deploy/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/hardhat-deploy/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/hardhat-deploy/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hardhat-deploy/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/hardhat-deploy/node_modules/zksync-web3": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/zksync-web3/-/zksync-web3-0.14.4.tgz", + "integrity": "sha512-kYehMD/S6Uhe1g434UnaMN+sBr9nQm23Ywn0EUP5BfQCsbjcr3ORuS68PosZw8xUTu3pac7G6YMSnNHk+fwzvg==", + "deprecated": "This package has been deprecated in favor of zksync-ethers@5.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ethers": "^5.7.0" + } + }, + "node_modules/hardhat-gas-reporter": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-2.3.0.tgz", + "integrity": "sha512-ySdA+044xMQv1BlJu5CYXToHzMexKFfIWxlQTBNNoerx1x96+d15IMdN01iQZ/TJ7NH2V5sU73bz77LoS/PEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/units": "^5.7.0", + "@solidity-parser/parser": "^0.20.1", + "axios": "^1.6.7", + "brotli-wasm": "^2.0.1", + "chalk": "4.1.2", + "cli-table3": "^0.6.3", + "ethereum-cryptography": "^2.1.3", + "glob": "^10.3.10", + "jsonschema": "^1.4.1", + "lodash": "^4.17.21", + "markdown-table": "2.0.0", + "sha1": "^1.1.1", + "viem": "^2.27.0" + }, + "peerDependencies": { + "hardhat": "^2.16.0" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat-gas-reporter/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/hardhat/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/hardhat/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/hardhat/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/hardhat/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/hardhat/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/hardhat/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hardhat/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hardhat/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hardhat/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/hardhat/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hardhat/node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/hardhat/node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/hardhat/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hardhat/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/hardhat/node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hardhat/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/hardhat/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/hardhat/node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/hardhat/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hardhat/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imul": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", + "integrity": "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/io-ts": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", + "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fp-ts": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", + "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=7.10.1" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/match-all": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/match-all/-/match-all-1.2.7.tgz", + "integrity": "sha512-qSpsBKarh55r9KyXzFC3xBLRf2GlGasba2em9kbpRsSlGvdTAqjx3QD0r3FKSARiW+OE4iMHYsolM3aX9n5djw==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz", + "integrity": "sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "dev": true, + "license": "MIT" + }, + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnemonist": { + "version": "0.38.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", + "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.0" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/murmur-128": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/murmur-128/-/murmur-128-0.2.1.tgz", + "integrity": "sha512-WseEgiRkI6aMFBbj8Cg9yBj/y+OdipwVC7zUo3W2W1JAJITwouUOtpqsmGSg67EQmwwSyod7hsVsWY5LsrfQVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "encode-utf8": "^1.0.2", + "fmix": "^0.1.0", + "imul": "^1.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-tfhe": { + "version": "1.4.0-alpha.3", + "resolved": "https://registry.npmjs.org/node-tfhe/-/node-tfhe-1.4.0-alpha.3.tgz", + "integrity": "sha512-oTcWL0OFA6t6BhScmDiGQ3VA8tU8T3EXCzIzpNxQxcuJDgQtiUF5CV6dgJLOrpWck4KCp1Bo/xLhv07uwn3q6Q==", + "dev": true, + "license": "BSD-3-Clause-Clear" + }, + "node_modules/node-tkms": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/node-tkms/-/node-tkms-0.12.4.tgz", + "integrity": "sha512-ZK+78E08l51NR8fKnvQggeYdiOFMj9vjvSZe5zvMjXvKH//hzrUic0xUJMkj+LWolQ2JamWEDwmaVxGQ5ZmUYQ==", + "dev": true, + "license": "BSD-3-Clause-Clear" + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.19" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/number-to-bn/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openzeppelin-confidential-contracts": { + "version": "0.2.0", + "resolved": "git+ssh://git@github.com/RegisGraptin/openzeppelin-confidential-contracts.git#60e815a49fb694854dfb966584f439e941fa51bb", + "integrity": "sha512-cXxrKmQUqNAWLjKfe3wy8mYtWWkcDixNAFbMSPvtATnEyb5U+5m9qmIkzUSGNfMO5119u1cz1lBew3jnLtV4eA==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordinal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", + "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ox": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", + "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-json/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-solidity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-2.2.0.tgz", + "integrity": "sha512-CjDixURb1rXdT+i/PYN3WykOPbusGBmbaZrxraEIfFMAGNtGhALOcoZ5UWhhMWbwUGtXz4c5pnjCzVaBNF/3CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/slang": "1.3.0", + "@solidity-parser/parser": "^0.20.2", + "semver": "^7.7.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": ">=3.0.0" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.2.tgz", + "integrity": "sha512-mNm/lblgES8UkVle8rGImXOz4TtL3eU3inHay/7TVchkKrb/lgcVvTK0+VAw8p5zQ0rgQsXm1j5dOlAAd+MeoA==", + "dev": true, + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/prettier-plugin-solidity/node_modules/@nomicfoundation/slang": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-1.3.0.tgz", + "integrity": "sha512-2PcwCF5cX2UjGi9cHd7E6XtLDaoyEVOTYt8WtOJ7GVpa68b+/sX0uFkb5/S67vxJb8eaVVFd2u6MTSVmE84QsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.2" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/recursive-readdir/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/recursive-readdir/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.0.tgz", + "integrity": "sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^11.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rlp": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", + "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "bn.js": "^5.2.0" + }, + "bin": { + "rlp": "bin/rlp" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "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==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sc-istanbul": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", + "integrity": "sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" + } + }, + "node_modules/sc-istanbul/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/sc-istanbul/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sc-istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sc-istanbul/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sc-istanbul/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sc-istanbul/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "dev": true, + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shelljs/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/solc": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", + "integrity": "sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/solhint": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-6.0.1.tgz", + "integrity": "sha512-Lew5nhmkXqHPybzBzkMzvvWkpOJSSLTkfTZwRriWvfR2naS4YW2PsjVGaoX9tZFmHh7SuS+e2GEGo5FPYYmJ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@solidity-parser/parser": "^0.20.2", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "antlr4": "^4.13.1-patch-1", + "ast-parents": "^0.0.1", + "better-ajv-errors": "^2.0.2", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "latest-version": "^7.0.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^7.5.2", + "table": "^6.8.1", + "text-table": "^0.2.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^2.8.3" + } + }, + "node_modules/solhint/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/solhint/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/solhint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/solhint/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solhint/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/solhint/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-ast": { + "version": "0.4.61", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.61.tgz", + "integrity": "sha512-OYBJYcYyG7gLV0VuXl9CUrvgJXjV/v0XnR4+1YomVe3q+QyENQXJJxAEASUz4vN6lMAl+C8RSRSr5MBAz09f6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/solidity-comments-darwin-arm64": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/solidity-comments-darwin-arm64/-/solidity-comments-darwin-arm64-0.1.1.tgz", + "integrity": "sha512-ze1+YboHm8tRJXCoFEsxtU1gpvQ3rCH55xMtBH6dtyh1/gz4qrKCOUBaAP+IHplbzCacZBn+Pz3UmMDOoGshRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-linux-x64-gnu": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/solidity-comments-linux-x64-gnu/-/solidity-comments-linux-x64-gnu-0.1.1.tgz", + "integrity": "sha512-oiU4yhh1Q9SeGXQ+/sogfTNoOkU8I8IvlIeotBQziTOonUHrxQk4E63kNiYGPDn5ZbB3BhKFmGHNRNrkufsxcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-coverage": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.16.tgz", + "integrity": "sha512-qKqgm8TPpcnCK0HCDLJrjbOA2tQNEJY4dHX/LSSQ9iwYFS973MwjtgYn2Iv3vfCEQJTj5xtm4cuUMzlJsJSMbg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@ethersproject/abi": "^5.0.9", + "@solidity-parser/parser": "^0.20.1", + "chalk": "^2.4.2", + "death": "^1.1.0", + "difflib": "^0.2.4", + "fs-extra": "^8.1.0", + "ghost-testrpc": "^0.0.2", + "global-modules": "^2.0.0", + "globby": "^10.0.1", + "jsonschema": "^1.2.4", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "node-emoji": "^1.10.0", + "pify": "^4.0.1", + "recursive-readdir": "^2.2.2", + "sc-istanbul": "^0.4.5", + "semver": "^7.3.4", + "shelljs": "^0.8.3", + "web3-utils": "^1.3.6" + }, + "bin": { + "solidity-coverage": "plugins/bin.js" + }, + "peerDependencies": { + "hardhat": "^2.11.0" + } + }, + "node_modules/solidity-coverage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/solidity-coverage/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/solidity-coverage/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/solidity-coverage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/solidity-coverage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/solidity-coverage/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/solidity-coverage/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/solidity-coverage/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/solidity-coverage/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/solidity-coverage/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/solidity-coverage/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-coverage/node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/solidity-coverage/node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/solidity-coverage/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/solidity-coverage/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-coverage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/solidity-coverage/node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/solidity-coverage/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-coverage/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", + "dev": true, + "optional": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "license": "WTFPL OR MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tfhe": { + "version": "1.4.0-alpha.3", + "resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz", + "integrity": "sha512-xdla7hi2WzLFIdAx2/ihRZ/bKlKcgDDabTJGtoqp1E5oqhLM1PzTXsJE0p7tW8+ebrvxiMGfbgMAWnU3f2ZAIQ==", + "dev": true, + "license": "BSD-3-Clause-Clear" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tkms": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/tkms/-/tkms-0.12.4.tgz", + "integrity": "sha512-IOX63KqSROQgRJApAYCU0Oi0kvgaMwoEEJH5hJuSYxeNMbG8tokNRfWbu2icjP5DzbzMqPdptmD6SbqyibxMvA==", + "dev": true, + "license": "BSD-3-Clause-Clear" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "bin": { + "write-markdown": "dist/write-markdown.js" + } + }, + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/ts-generator": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ts-generator/-/ts-generator-0.1.1.tgz", + "integrity": "sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mkdirp": "^0.5.2", + "@types/prettier": "^2.1.1", + "@types/resolve": "^0.0.8", + "chalk": "^2.4.1", + "glob": "^7.1.2", + "mkdirp": "^0.5.1", + "prettier": "^2.1.2", + "resolve": "^1.8.1", + "ts-essentials": "^1.0.0" + }, + "bin": { + "ts-generator": "dist/cli/run.js" + } + }, + "node_modules/ts-generator/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-generator/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ts-generator/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-generator/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ts-generator/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-generator/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-generator/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-generator/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-generator/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ts-generator/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/ts-generator/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-generator/node_modules/ts-essentials": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz", + "integrity": "sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsort": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", + "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/typechain/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typechain/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typechain/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typechain/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/typechain/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/viem": { + "version": "2.39.0", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.39.0.tgz", + "integrity": "sha512-rCN+IfnMESlrg/iPyyVL+M9NS/BHzyyNy72470tFmbTuscY3iPaZGMtJDcHKKV8TC6HV9DjWk0zWX6cpu0juyA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.1.0", + "isows": "1.0.7", + "ox": "0.9.6", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/web3-utils": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereum-cryptography": "^2.1.2", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "license": "MIT", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/protocol-contracts/confidential-wrapper/package.json b/protocol-contracts/confidential-wrapper/package.json new file mode 100644 index 0000000000..93fdf7757b --- /dev/null +++ b/protocol-contracts/confidential-wrapper/package.json @@ -0,0 +1,107 @@ +{ + "name": "fhevm-hardhat-template", + "description": "Hardhat-based template for developing FHEVM Solidity smart contracts", + "version": "0.1.0", + "engines": { + "node": ">=20", + "npm": ">=7.0.0" + }, + "license": "BSD-3-Clause-Clear", + "homepage": "https://github.com/zama-ai/fhevm-hardhat-template/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/zama-ai/fhevm-hardhat-template.git" + }, + "keywords": [ + "fhevm", + "zama", + "eth", + "ethereum", + "dapps", + "wallet", + "web3", + "typescript", + "hardhat" + ], + "dependencies": { + "@fhevm/solidity": "^0.9.1", + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@zama-fhe/oracle-solidity": "^0.1.0", + "encrypted-types": "^0.0.4", + "express": "^5.1.0", + "openzeppelin-confidential-contracts": "github:RegisGraptin/openzeppelin-confidential-contracts#60e815a49fb694854dfb966584f439e941fa51bb" + }, + "devDependencies": { + "@fhevm/hardhat-plugin": "^0.3.0-1", + "@nomicfoundation/hardhat-chai-matchers": "^2.1.0", + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-network-helpers": "^1.1.0", + "@nomicfoundation/hardhat-verify": "^2.1.0", + "@openzeppelin/hardhat-upgrades": "^3.9.1", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", + "@types/node": "^20.19.8", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "@zama-fhe/relayer-sdk": "^0.3.0-5", + "chai": "^4.5.0", + "chai-as-promised": "^8.0.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "ethers": "^6.15.0", + "hardhat": "^2.26.0", + "hardhat-deploy": "^0.11.45", + "hardhat-gas-reporter": "^2.3.0", + "mocha": "^11.7.1", + "prettier": "^3.6.2", + "prettier-plugin-solidity": "^2.1.0", + "rimraf": "^6.0.1", + "solhint": "^6.0.0", + "solidity-coverage": "^0.8.16", + "ts-generator": "^0.1.1", + "ts-node": "^10.9.2", + "typechain": "^8.3.2", + "typescript": "^5.8.3" + }, + "files": [ + "contracts" + ], + "scripts": { + "clean": "rimraf ./fhevmTemp ./artifacts ./cache ./coverage ./types ./coverage.json ./dist && npm run typechain", + "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", + "coverage": "cross-env SOLIDITY_COVERAGE=true hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"test/**/*.ts\" && npm run typechain", + "lint": "npm run lint:sol && npm run lint:ts && npm run prettier:check", + "lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"", + "lint:ts": "eslint --ignore-path ./.eslintignore --ext .js,.ts .", + "postcompile": "npm run typechain", + "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", + "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", + "test": "hardhat test", + "test:sepolia": "hardhat test --network sepolia", + "build:ts": "tsc --project tsconfig.json", + "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain" + }, + "overrides": { + "ws@>=7.0.0 <7.5.10": ">=7.5.10", + "axios@>=1.3.2 <=1.7.3": ">=1.7.4", + "elliptic@>=4.0.0 <=6.5.6": ">=6.5.7", + "elliptic@>=2.0.0 <=6.5.6": ">=6.5.7", + "elliptic@>=5.2.1 <=6.5.6": ">=6.5.7", + "micromatch@<4.0.8": ">=4.0.8", + "elliptic@<6.6.0": ">=6.6.0", + "elliptic@<6.5.6": ">=6.5.6", + "undici@>=6.0.0 <6.21.1": "6.21.1 <7.0.0", + "undici@>=4.5.0 <5.28.5": "5.28.5 <7.0.0", + "elliptic@<=6.6.0": ">=6.6.1", + "tar-fs@>=2.0.0 <2.1.2": ">=2.1.2", + "axios@>=1.0.0 <1.8.2": ">=1.8.2", + "axios@<0.29.1": ">=0.29.1", + "cookie@<0.7.0": ">=0.7.0" + } +} diff --git a/protocol-contracts/confidential-wrapper/tasks/FHECounter.ts b/protocol-contracts/confidential-wrapper/tasks/FHECounter.ts new file mode 100644 index 0000000000..1263ad7918 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/tasks/FHECounter.ts @@ -0,0 +1,184 @@ +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +/** + * Tutorial: Deploy and Interact Locally (--network localhost) + * =========================================================== + * + * 1. From a separate terminal window: + * + * npx hardhat node + * + * 2. Deploy the FHECounter contract + * + * npx hardhat --network localhost deploy + * + * 3. Interact with the FHECounter contract + * + * npx hardhat --network localhost task:decrypt-count + * npx hardhat --network localhost task:increment --value 2 + * npx hardhat --network localhost task:decrement --value 1 + * npx hardhat --network localhost task:decrypt-count + * + * + * Tutorial: Deploy and Interact on Sepolia (--network sepolia) + * =========================================================== + * + * 1. Deploy the FHECounter contract + * + * npx hardhat --network sepolia deploy + * + * 2. Interact with the FHECounter contract + * + * npx hardhat --network sepolia task:decrypt-count + * npx hardhat --network sepolia task:increment --value 2 + * npx hardhat --network sepolia task:decrement --value 1 + * npx hardhat --network sepolia task:decrypt-count + * + */ + +/** + * Example: + * - npx hardhat --network localhost task:address + * - npx hardhat --network sepolia task:address + */ +task("task:address", "Prints the FHECounter address").setAction(async function (_taskArguments: TaskArguments, hre) { + const { deployments } = hre; + + const fheCounter = await deployments.get("FHECounter"); + + console.log("FHECounter address is " + fheCounter.address); +}); + +/** + * Example: + * - npx hardhat --network localhost task:decrypt-count + * - npx hardhat --network sepolia task:decrypt-count + */ +task("task:decrypt-count", "Calls the getCount() function of Counter Contract") + .addOptionalParam("address", "Optionally specify the Counter contract address") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments, fhevm } = hre; + + await fhevm.initializeCLIApi(); + + const FHECounterDeployement = taskArguments.address + ? { address: taskArguments.address } + : await deployments.get("FHECounter"); + console.log(`FHECounter: ${FHECounterDeployement.address}`); + + const signers = await ethers.getSigners(); + + const fheCounterContract = await ethers.getContractAt("FHECounter", FHECounterDeployement.address); + + const encryptedCount = await fheCounterContract.getCount(); + if (encryptedCount === ethers.ZeroHash) { + console.log(`encrypted count: ${encryptedCount}`); + console.log("clear count : 0"); + return; + } + + const clearCount = await fhevm.userDecryptEuint( + FhevmType.euint32, + encryptedCount, + FHECounterDeployement.address, + signers[0], + ); + console.log(`Encrypted count: ${encryptedCount}`); + console.log(`Clear count : ${clearCount}`); + }); + +/** + * Example: + * - npx hardhat --network localhost task:increment --value 1 + * - npx hardhat --network sepolia task:increment --value 1 + */ +task("task:increment", "Calls the increment() function of FHECounter Contract") + .addOptionalParam("address", "Optionally specify the FHECounter contract address") + .addParam("value", "The increment value") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments, fhevm } = hre; + + const value = parseInt(taskArguments.value); + if (!Number.isInteger(value)) { + throw new Error(`Argument --value is not an integer`); + } + + await fhevm.initializeCLIApi(); + + const FHECounterDeployement = taskArguments.address + ? { address: taskArguments.address } + : await deployments.get("FHECounter"); + console.log(`FHECounter: ${FHECounterDeployement.address}`); + + const signers = await ethers.getSigners(); + + const fheCounterContract = await ethers.getContractAt("FHECounter", FHECounterDeployement.address); + + // Encrypt the value passed as argument + const encryptedValue = await fhevm + .createEncryptedInput(FHECounterDeployement.address, signers[0].address) + .add32(value) + .encrypt(); + + const tx = await fheCounterContract + .connect(signers[0]) + .increment(encryptedValue.handles[0], encryptedValue.inputProof); + console.log(`Wait for tx:${tx.hash}...`); + + const receipt = await tx.wait(); + console.log(`tx:${tx.hash} status=${receipt?.status}`); + + const newEncryptedCount = await fheCounterContract.getCount(); + console.log("Encrypted count after increment:", newEncryptedCount); + + console.log(`FHECounter increment(${value}) succeeded!`); + }); + +/** + * Example: + * - npx hardhat --network localhost task:decrement --value 1 + * - npx hardhat --network sepolia task:decrement --value 1 + */ +task("task:decrement", "Calls the decrement() function of FHECounter Contract") + .addOptionalParam("address", "Optionally specify the FHECounter contract address") + .addParam("value", "The decrement value") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments, fhevm } = hre; + + const value = parseInt(taskArguments.value); + if (!Number.isInteger(value)) { + throw new Error(`Argument --value is not an integer`); + } + + await fhevm.initializeCLIApi(); + + const FHECounterDeployement = taskArguments.address + ? { address: taskArguments.address } + : await deployments.get("FHECounter"); + console.log(`FHECounter: ${FHECounterDeployement.address}`); + + const signers = await ethers.getSigners(); + + const fheCounterContract = await ethers.getContractAt("FHECounter", FHECounterDeployement.address); + + // Encrypt the value passed as argument + const encryptedValue = await fhevm + .createEncryptedInput(FHECounterDeployement.address, signers[0].address) + .add32(value) + .encrypt(); + + const tx = await fheCounterContract + .connect(signers[0]) + .decrement(encryptedValue.handles[0], encryptedValue.inputProof); + console.log(`Wait for tx:${tx.hash}...`); + + const receipt = await tx.wait(); + console.log(`tx:${tx.hash} status=${receipt?.status}`); + + const newEncryptedCount = await fheCounterContract.getCount(); + console.log("Encrypted count after decrement:", newEncryptedCount); + + console.log(`FHECounter decrement(${value}) succeeded!`); + }); diff --git a/protocol-contracts/confidential-wrapper/tasks/accounts.ts b/protocol-contracts/confidential-wrapper/tasks/accounts.ts new file mode 100644 index 0000000000..c7f973316a --- /dev/null +++ b/protocol-contracts/confidential-wrapper/tasks/accounts.ts @@ -0,0 +1,9 @@ +import { task } from "hardhat/config"; + +task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => { + const accounts = await hre.ethers.getSigners(); + + for (const account of accounts) { + console.log(`${account.address}`); + } +}); diff --git a/protocol-contracts/confidential-wrapper/test/AdminProvider.ts b/protocol-contracts/confidential-wrapper/test/AdminProvider.ts new file mode 100644 index 0000000000..4a6fad17c1 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/AdminProvider.ts @@ -0,0 +1,163 @@ +import { ethers } from "hardhat"; +import { AdminProvider, FeeManager, SanctionsList } from "../types"; +import { expect } from "chai"; +import { getSigners, Signers } from "./signers"; +import { deployAdminProviderFixture, deployFeeManagerFixture, deploySanctionsListFixture } from "./fixtures"; + + +describe("AdminProvider", function () { + let signers: Signers; + let adminProvider: AdminProvider; + let adminProviderAddress: string; + let feeManager: FeeManager; + let feeManagerAddress: string; + let sanctionsList: SanctionsList; + let sanctionsListAddress: string; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async () => { + ({ adminProvider, adminProviderAddress, feeManager, feeManagerAddress, sanctionsList, sanctionsListAddress } = await deployAdminProviderFixture(signers)); + }); + + describe("Deployment", function () { + it("should deploy with correct owner", async function () { + expect(await adminProvider.owner()).to.equal(signers.deployer.address); + }); + + it("should have sanctions list from constructor", async function () { + expect(await adminProvider.sanctionsList()).to.equal(sanctionsListAddress); + }); + + it("should have fee manager from constructor", async function () { + expect(await adminProvider.feeManager()).to.equal(feeManagerAddress); + }); + + it("should revert if fee manager is zero address", async function () { + const AdminProviderFactory = await ethers.getContractFactory("AdminProvider"); + + await expect( + AdminProviderFactory.deploy(ethers.ZeroAddress, sanctionsList, signers.regulator.address) + ).to.be.revertedWithCustomError(AdminProviderFactory, "ZeroAddressFeeManager"); + }); + + it("should revert if sanctions list is zero address", async function () { + const AdminProviderFactory = await ethers.getContractFactory("AdminProvider"); + + await expect( + AdminProviderFactory.deploy(feeManager, ethers.ZeroAddress, signers.regulator.address) + ).to.be.revertedWithCustomError(AdminProviderFactory, "ZeroAddressSanctionsList"); + }); + + it("should revert if regulator is zero address", async function () { + const AdminProviderFactory = await ethers.getContractFactory("AdminProvider"); + + await expect( + AdminProviderFactory.deploy(feeManager, sanctionsList, ethers.ZeroAddress) + ).to.be.revertedWithCustomError(AdminProviderFactory, "ZeroAddressRegulator"); + }); + }); + + describe("Sanctions List Management", function () { + it("should allow owner to set sanctions list", async function () { + const { sanctionsList: newSanctionsList, sanctionsListAddress: newSanctionsListAddress } = await deploySanctionsListFixture(); + + await expect(adminProvider.setSanctionsList(newSanctionsList)) + .to.emit(adminProvider, "SanctionsListUpdated") + .withArgs(sanctionsListAddress, newSanctionsListAddress); + + expect(await adminProvider.sanctionsList()).to.equal(newSanctionsListAddress); + }); + + it("should prevent non-owner from setting sanctions list", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + + await expect( + adminProvider.connect(signers.alice).setSanctionsList(sanctionsList) + ).to.be.revertedWithCustomError(adminProvider, "OwnableUnauthorizedAccount"); + }); + + it("should emit event when updating existing sanctions list", async function () { + const { sanctionsList: sanctionsList1, sanctionsListAddress: address1 } = await deploySanctionsListFixture(); + const { sanctionsList: sanctionsList2, sanctionsListAddress: address2 } = await deploySanctionsListFixture(); + + await adminProvider.setSanctionsList(sanctionsList1); + + await expect(adminProvider.setSanctionsList(sanctionsList2)) + .to.emit(adminProvider, "SanctionsListUpdated") + .withArgs(address1, address2); + }); + + it("should revert if sanctions list is zero address", async function () { + await expect( + adminProvider.setSanctionsList(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(adminProvider, "ZeroAddressSanctionsList"); + }); + }); + + describe("Fee Manager Management", function () { + it("should allow owner to set fee manager", async function () { + const oldFeeManager = await adminProvider.feeManager(); + const { feeManager, feeManagerAddress } = await deployFeeManagerFixture(signers.royalties); + + await expect(adminProvider.setFeeManager(feeManager)) + .to.emit(adminProvider, "FeeManagerUpdated") + .withArgs(oldFeeManager, feeManagerAddress); + + expect(await adminProvider.feeManager()).to.equal(feeManagerAddress); + }); + + it("should prevent non-owner from setting fee manager", async function () { + const { feeManagerAddress } = await deployFeeManagerFixture(signers.royalties); + + await expect( + adminProvider.connect(signers.alice).setFeeManager(feeManagerAddress) + ).to.be.revertedWithCustomError(adminProvider, "OwnableUnauthorizedAccount"); + }); + + it("should revert if fee manager is zero address", async function () { + await expect( + adminProvider.setFeeManager(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(adminProvider, "ZeroAddressFeeManager"); + }); + }); + + describe("Regulator Management", function () { + it("should allow owner to set regulator", async function () { + const oldRegulator = await adminProvider.regulator(); + const newRegulator = signers.alice.address; + + await expect(adminProvider.setRegulator(newRegulator)) + .to.emit(adminProvider, "RegulatorUpdated") + .withArgs(oldRegulator, newRegulator); + + expect(await adminProvider.regulator()).to.equal(newRegulator); + }); + + it("should prevent non-owner from setting regulator", async function () { + await expect( + adminProvider.connect(signers.alice).setRegulator(signers.bob.address) + ).to.be.revertedWithCustomError(adminProvider, "OwnableUnauthorizedAccount"); + }); + + it("should revert if regulator is zero address", async function () { + await expect( + adminProvider.setRegulator(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(adminProvider, "ZeroAddressRegulator"); + }); + }); + + describe("Ownership", function () { + it("should support two-step ownership transfer", async function () { + await adminProvider.transferOwnership(signers.alice.address); + expect(await adminProvider.pendingOwner()).to.equal(signers.alice.address); + expect(await adminProvider.owner()).to.equal(signers.deployer.address); + + await adminProvider.connect(signers.alice).acceptOwnership(); + expect(await adminProvider.owner()).to.equal(signers.alice.address); + expect(await adminProvider.pendingOwner()).to.equal(ethers.ZeroAddress); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/DeploymentCoordinator.AdminProvider.ts b/protocol-contracts/confidential-wrapper/test/DeploymentCoordinator.AdminProvider.ts new file mode 100644 index 0000000000..5bcbfaf7b3 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/DeploymentCoordinator.AdminProvider.ts @@ -0,0 +1,95 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { getSigners, Signers } from "./signers"; +import { deployWrapperFixture, deployAdminProviderFixture, deployTestERC20Fixture } from "./fixtures"; +import { deployConfidentialToken } from "./utils"; +import { + DeploymentCoordinator, + AdminProvider, + WrapperUpgradeable, + RegulatedERC7984Upgradeable, + TestERC20, +} from "../types"; + +describe("DeploymentCoordinator - Dynamic AdminProvider Update", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let adminProvider: AdminProvider; + let tokenA: TestERC20; + let tokenB: TestERC20; + let wrapperA: WrapperUpgradeable; + let wrapperB: WrapperUpgradeable; + let cTokenA: RegulatedERC7984Upgradeable; + let cTokenB: RegulatedERC7984Upgradeable; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + // Deploy the full system using fixture + ({ coordinator, adminProvider } = await deployWrapperFixture(signers)); + + // Deploy two test tokens + tokenA = await deployTestERC20Fixture("TKNA", 18); + tokenB = await deployTestERC20Fixture("TKNB", 6); + + // Deploy wrapper/cToken pairs for both tokens + const { wrapper: wrapA, cTokenAddress: cTokenAddressA } = await deployConfidentialToken( + coordinator, + tokenA, + signers.deployer + ); + wrapperA = wrapA; + cTokenA = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddressA); + + const { wrapper: wrapB, cTokenAddress: cTokenAddressB } = await deployConfidentialToken( + coordinator, + tokenB, + signers.deployer + ); + wrapperB = wrapB; + cTokenB = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddressB); + }); + + it("should update AdminProvider for all deployed wrappers and tokens simultaneously", async function () { + // Verify all contracts initially use the same AdminProvider + const initialAdminProviderAddress = await adminProvider.getAddress(); + + expect(await wrapperA.adminProvider()).to.equal(initialAdminProviderAddress); + expect(await wrapperB.adminProvider()).to.equal(initialAdminProviderAddress); + expect(await cTokenA.adminProvider()).to.equal(initialAdminProviderAddress); + expect(await cTokenB.adminProvider()).to.equal(initialAdminProviderAddress); + + // Verify initial regulator + const initialRegulator = await adminProvider.regulator(); + expect(await cTokenA.regulator()).to.equal(initialRegulator); + expect(await cTokenB.regulator()).to.equal(initialRegulator); + + // Deploy a new AdminProvider with a different regulator + const { adminProvider: newAdminProvider } = await deployAdminProviderFixture(signers); + await newAdminProvider.setRegulator(signers.alice.address); // Use Alice as new regulator + + const newAdminProviderAddress = await newAdminProvider.getAddress(); + + // Update AdminProvider in coordinator - this should affect ALL deployed contracts + await expect(coordinator.setAdminProvider(newAdminProvider)) + .to.emit(coordinator, "AdminProviderUpdated") + .withArgs(initialAdminProviderAddress, newAdminProviderAddress); + + // Verify all wrappers and tokens now see the new AdminProvider WITHOUT any upgrade + expect(await wrapperA.adminProvider()).to.equal(newAdminProviderAddress); + expect(await wrapperB.adminProvider()).to.equal(newAdminProviderAddress); + expect(await cTokenA.adminProvider()).to.equal(newAdminProviderAddress); + expect(await cTokenB.adminProvider()).to.equal(newAdminProviderAddress); + + // Verify they all see the new regulator + expect(await cTokenA.regulator()).to.equal(signers.alice.address); + expect(await cTokenB.regulator()).to.equal(signers.alice.address); + + // Verify the contracts maintain their own independent storage + expect(await wrapperA.originalToken()).to.equal(await tokenA.getAddress()); + expect(await wrapperB.originalToken()).to.equal(await tokenB.getAddress()); + expect(await wrapperA.originalToken()).to.not.equal(await wrapperB.originalToken()); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/ERC7984Upgradeable.ts b/protocol-contracts/confidential-wrapper/test/ERC7984Upgradeable.ts new file mode 100644 index 0000000000..d6fe08a9eb --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/ERC7984Upgradeable.ts @@ -0,0 +1,643 @@ +// ported from: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/blob/e70062127e25cf17a8ecac2fda31320ae71c57d0/test/token/ERC7984/ERC7984.test.ts + +import { expect } from 'chai'; +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { ethers, fhevm, upgrades } from 'hardhat'; +import fs from 'fs'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { Addressable, Signer } from 'ethers'; + +export async function getAclAddress() { + return (await fhevm.getRelayerMetadata()).ACLAddress; +} + +async function allowHandle(hre: HardhatRuntimeEnvironment, from: Signer, to: Addressable, handle: string) { + const acl_abi = JSON.parse( + fs.readFileSync('node_modules/@fhevm/host-contracts/artifacts/contracts/ACL.sol/ACL.json', 'utf8'), + ).abi; + const aclContract = await hre.ethers.getContractAt(acl_abi, await getAclAddress()); + + await aclContract.connect(from).allow(handle, to); +} + +const name = 'ERC7984Mock'; +const symbol = 'E7984M'; +const contractURI = 'contractURI'; + +describe('ERC7984', function () { + beforeEach(async function () { + const accounts = await ethers.getSigners(); + const [holder, recipient, operator] = accounts; + + const ERC7984 = await ethers.getContractFactory("ERC7984MockUpgradeable"); + const erc7984 = await upgrades.deployProxy(ERC7984, [name, symbol, contractURI]); + await erc7984.waitForDeployment(); + + this.accounts = accounts.slice(3); + this.holder = holder; + this.recipient = recipient; + this.token = erc7984; + this.operator = operator; + + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(1000) + .encrypt(); + + await this.token + .connect(this.holder) + .mint(this.holder, encryptedInput.handles[0], encryptedInput.inputProof); + }); + + describe('initializer', function () { + it('sets the name', async function () { + expect(await this.token.name()).to.equal(name); + }); + + it('sets the symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol); + }); + + it('sets the contractURI', async function () { + expect(await this.token.contractURI()).to.equal(contractURI); + }); + + it("cannot call init twice", async function () { + await expect( + this.token.initialize("Naraggara", "NARA", "uri"), + ).to.be.revertedWithCustomError(this.token, "InvalidInitialization"); + }); + + it("cannot call init unchained twice", async function () { + await expect( + this.token.initialize("Naraggara", "NARA", "uri"), + ).to.be.revertedWithCustomError(this.token, "InvalidInitialization"); + }); + }); + + describe('confidentialBalanceOf', function () { + it('handle can be reencryped by owner', async function () { + const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), + ).to.eventually.equal(1000); + }); + + it('handle cannot be reencryped by non-owner', async function () { + const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.accounts[0]), + ).to.be.rejectedWith(generateReencryptionErrorMessage(balanceOfHandleHolder, this.accounts[0].address)); + }); + }); + + describe('mint', function () { + for (const existingUser of [false, true]) { + it(`to ${existingUser ? 'existing' : 'new'} user`, async function () { + if (existingUser) { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(1000) + .encrypt(); + + await this.token + .connect(this.holder) + .mint(this.holder, encryptedInput.handles[0], encryptedInput.inputProof); + } + + const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), + ).to.eventually.equal(existingUser ? 2000 : 1000); + + // Check total supply + // const totalSupplyHandle = await this.token.confidentialTotalSupply(); + // await expect( + // fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, this.token.target, this.holder), + // ).to.eventually.equal(existingUser ? 2000 : 1000); + }); + } + + it('from zero address', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(400) + .encrypt(); + + await expect( + this.token + .connect(this.holder) + .mint(ethers.ZeroAddress, encryptedInput.handles[0], encryptedInput.inputProof), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('burn', function () { + for (const sufficientBalance of [false, true]) { + it(`from a user with ${sufficientBalance ? 'sufficient' : 'insufficient'} balance`, async function () { + const burnAmount = sufficientBalance ? 400 : 1100; + + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(burnAmount) + .encrypt(); + + await this.token + .connect(this.holder) + .burn(this.holder, encryptedInput.handles[0], encryptedInput.inputProof); + + const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), + ).to.eventually.equal(sufficientBalance ? 600 : 1000); + + // Check total supply + // const totalSupplyHandle = await this.token.confidentialTotalSupply(); + // await expect( + // fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, this.token.target, this.holder), + // ).to.eventually.equal(sufficientBalance ? 600 : 1000); + }); + } + + it('from zero address', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(400) + .encrypt(); + + await expect( + this.token + .connect(this.holder) + .burn(ethers.ZeroAddress, encryptedInput.handles[0], encryptedInput.inputProof), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('transfer', function () { + for (const asSender of [true, false]) { + describe(asSender ? 'as sender' : 'as operator', function () { + beforeEach(async function () { + if (!asSender) { + const timestamp = (await ethers.provider.getBlock('latest'))!.timestamp + 100; + await this.token.connect(this.holder).setOperator(this.operator.address, timestamp); + } + }); + + if (!asSender) { + for (const withCallback of [false, true]) { + describe(withCallback ? 'with callback' : 'without callback', function () { + let encryptedInput: { handles: string[]; inputProof: string }; + let params: (string | Uint8Array)[]; + + beforeEach(async function () { + encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.operator.address) + .add64(100) + .encrypt(); + + params = [ + this.holder.address, + this.recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ]; + if (withCallback) { + params.push('0x'); + } + }); + + it('without operator approval should fail', async function () { + await this.token.connect(this.holder).setOperator(this.operator, 0); + + await expect( + this.token + .connect(this.operator) + [ + withCallback + ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' + : 'confidentialTransferFrom(address,address,bytes32,bytes)' + ](...params), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedSpender') + .withArgs(this.holder.address, this.operator.address); + }); + + it('should be successful', async function () { + await this.token + .connect(this.operator) + [ + withCallback + ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' + : 'confidentialTransferFrom(address,address,bytes32,bytes)' + ](...params); + }); + }); + } + } + + // Edge cases to run with sender as caller + if (asSender) { + it('with no balance should revert', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.recipient.address) + .add64(100) + .encrypt(); + + await expect( + this.token + .connect(this.recipient) + ['confidentialTransfer(address,bytes32,bytes)']( + this.holder.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984ZeroBalance') + .withArgs(this.recipient.address); + }); + + it('to zero address', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(100) + .encrypt(); + + await expect( + this.token + .connect(this.holder) + ['confidentialTransfer(address,bytes32,bytes)']( + ethers.ZeroAddress, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + } + + for (const sufficientBalance of [false, true]) { + it(`${sufficientBalance ? 'sufficient' : 'insufficient'} balance`, async function () { + const transferAmount = sufficientBalance ? 400 : 1100; + + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, asSender ? this.holder.address : this.operator.address) + .add64(transferAmount) + .encrypt(); + + let tx; + if (asSender) { + tx = await this.token + .connect(this.holder) + ['confidentialTransfer(address,bytes32,bytes)']( + this.recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ); + } else { + tx = await this.token + .connect(this.operator) + ['confidentialTransferFrom(address,address,bytes32,bytes)']( + this.holder.address, + this.recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ); + } + const transferEvent = (await tx.wait())!.logs.filter((log) => log.address === this.token.target)[0]; + expect(transferEvent.args[0]).to.equal(this.holder.address); + expect(transferEvent.args[1]).to.equal(this.recipient.address); + + const transferAmountHandle = transferEvent.args[2]; + const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + const recipientBalanceHandle = await this.token.confidentialBalanceOf(this.recipient); + + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.holder), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.recipient), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + // Other can not reencrypt the transfer amount + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.operator), + ).to.be.rejectedWith(generateReencryptionErrorMessage(transferAmountHandle, this.operator.address)); + + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, holderBalanceHandle, this.token.target, this.holder), + ).to.eventually.equal(1000 - (sufficientBalance ? transferAmount : 0)); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, recipientBalanceHandle, this.token.target, this.recipient), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + }); + } + }); + } + + describe('without input proof', function () { + for (const [usingTransferFrom, withCallback] of [false, true].flatMap(val => [ + [val, false], + [val, true], + ])) { + describe(`using ${usingTransferFrom ? 'confidentialTransferFrom' : 'confidentialTransfer'} ${ + withCallback ? 'with callback' : '' + }`, function () { + async function callTransfer(contract: ERC7984Upgradeable, from: HardhatEthersSigner, to: HardhatEthersSigner, amount: bigint, sender: HardhatEthersSigner = from) { + const functionParams = [to, amount]; + + if (withCallback) { + functionParams.push('0x'); + if (usingTransferFrom) { + functionParams.unshift(from); + await contract.connect(sender).confidentialTransferFromAndCall(...functionParams); + } else { + await contract.connect(sender).confidentialTransferAndCall(...functionParams); + } + } else { + if (usingTransferFrom) { + functionParams.unshift(from); + await contract.connect(sender).confidentialTransferFrom(...functionParams); + } else { + await contract.connect(sender).confidentialTransfer(...functionParams); + } + } + } + + it('full balance', async function () { + const fullBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + + await callTransfer(this.token, this.holder, this.recipient, fullBalanceHandle); + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await this.token.confidentialBalanceOf(this.recipient), + this.token.target, + this.recipient, + ), + ).to.eventually.equal(1000); + }); + + it('other user balance should revert', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(100) + .encrypt(); + + await this.token + .connect(this.holder) + ['mint(address,bytes32,bytes)'](this.recipient, encryptedInput.handles[0], encryptedInput.inputProof); + + const recipientBalanceHandle = await this.token.confidentialBalanceOf(this.recipient); + await expect(callTransfer(this.token, this.holder, this.recipient, recipientBalanceHandle)) + .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedUseOfEncryptedAmount') + .withArgs(recipientBalanceHandle, this.holder); + }); + + if (usingTransferFrom) { + describe('without operator approval', function () { + beforeEach(async function () { + await this.token.connect(this.holder).setOperator(this.operator.address, 0); + await allowHandle(hre, this.holder, this.operator, await this.token.confidentialBalanceOf(this.holder)); + }); + + it('should revert', async function () { + await expect( + callTransfer( + this.token, + this.holder, + this.recipient, + await this.token.confidentialBalanceOf(this.holder), + this.operator, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedSpender') + .withArgs(this.holder.address, this.operator.address); + }); + }); + } + }); + } + }); + + it('internal function reverts on from address zero', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(100) + .encrypt(); + + await expect( + this.token + .connect(this.holder) + ['transfer(address,address,bytes32,bytes)']( + ethers.ZeroAddress, + this.recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('transfer with callback', function () { + beforeEach(async function () { + this.recipientContract = await ethers.deployContract('ERC7984ReceiverMock'); + + this.encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(1000) + .encrypt(); + }); + + for (const callbackSuccess of [false, true]) { + it(`with callback running ${callbackSuccess ? 'successfully' : 'unsuccessfully'}`, async function () { + const tx = await this.token + .connect(this.holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + this.recipientContract.target, + this.encryptedInput.handles[0], + this.encryptedInput.inputProof, + ethers.AbiCoder.defaultAbiCoder().encode(['bool'], [callbackSuccess]), + ); + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await this.token.confidentialBalanceOf(this.holder), + this.token.target, + this.holder, + ), + ).to.eventually.equal(callbackSuccess ? 0 : 1000); + + // Verify event contents + await expect(tx).to.emit(this.recipientContract, 'ConfidentialTransferCallback').withArgs(callbackSuccess); + const transferEvents = (await tx.wait())!.logs.filter((log) => log.address === this.token.target); + + const outboundTransferEvent = transferEvents[0]; + const inboundTransferEvent = transferEvents[1]; + + expect(outboundTransferEvent.args[0]).to.equal(this.holder.address); + expect(outboundTransferEvent.args[1]).to.equal(this.recipientContract.target); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, outboundTransferEvent.args[2], this.token.target, this.holder), + ).to.eventually.equal(1000); + + expect(inboundTransferEvent.args[0]).to.equal(this.recipientContract.target); + expect(inboundTransferEvent.args[1]).to.equal(this.holder.address); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, inboundTransferEvent.args[2], this.token.target, this.holder), + ).to.eventually.equal(callbackSuccess ? 0 : 1000); + }); + } + + it('with callback reverting without a reason', async function () { + await expect( + this.token + .connect(this.holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + this.recipientContract.target, + this.encryptedInput.handles[0], + this.encryptedInput.inputProof, + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') + .withArgs(this.recipientContract.target); + }); + + it('with callback reverting with a custom error', async function () { + await expect( + this.token + .connect(this.holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + this.recipientContract.target, + this.encryptedInput.handles[0], + this.encryptedInput.inputProof, + ethers.AbiCoder.defaultAbiCoder().encode(['uint8'], [2]), + ), + ) + .to.be.revertedWithCustomError(this.recipientContract, 'InvalidInput') + .withArgs(2); + }); + + it('to an EOA', async function () { + await this.token + .connect(this.holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + this.recipient, + this.encryptedInput.handles[0], + this.encryptedInput.inputProof, + '0x', + ); + + const balanceOfHandle = await this.token.confidentialBalanceOf(this.recipient); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandle, this.token.target, this.recipient), + ).to.eventually.equal(1000); + }); + }); + + describe('disclose', function () { + let expectedAmount: any; + let expectedHandle: any; + let requester: any; + + beforeEach(async function () { + expectedAmount = undefined; + expectedHandle = undefined; + requester = undefined; + }); + + it('user balance', async function () { + const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + + await this.token.connect(this.holder).requestDiscloseEncryptedAmount(holderBalanceHandle); + + requester = this.holder.address; + expectedAmount = 1000n; + expectedHandle = holderBalanceHandle; + }); + + it('transaction amount', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(400) + .encrypt(); + + const tx = await this.token['confidentialTransfer(address,bytes32,bytes)']( + this.recipient, + encryptedInput.handles[0], + encryptedInput.inputProof, + ); + + const transferEvent = (await tx.wait()).logs.filter((log: any) => log.address === this.token.target)[0]; + const transferAmount = transferEvent.args[2]; + + await this.token.connect(this.recipient).requestDiscloseEncryptedAmount(transferAmount); + + requester = this.recipient.address; + expectedAmount = 400n; + expectedHandle = transferAmount; + }); + + it("other user's balance", async function () { + const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + + await expect(this.token.connect(this.recipient).requestDiscloseEncryptedAmount(holderBalanceHandle)) + .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedUseOfEncryptedAmount') + .withArgs(holderBalanceHandle, this.recipient); + }); + + it('invalid signature reverts', async function () { + const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + await this.token.connect(this.holder).requestDiscloseEncryptedAmount(holderBalanceHandle); + + await expect(this.token.connect(this.holder).discloseEncryptedAmount(holderBalanceHandle, 0, '0x')).to + .be.reverted; + }); + + afterEach(async function () { + if (expectedHandle === undefined || expectedAmount === undefined) return; + + const amountDiscloseRequestedEvent = ( + await this.token.queryFilter(this.token.filters.AmountDiscloseRequested()) + )[0]; + + expect(expectedHandle).to.equal(amountDiscloseRequestedEvent.args[0]); + expect(requester).to.equal(amountDiscloseRequestedEvent.args[1]); + + const publicDecryptResults = await fhevm.publicDecrypt([expectedHandle]); + + const tx = await this.token + .connect(this.holder) + .discloseEncryptedAmount( + expectedHandle, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + await tx.wait(); + + // Check that event was correctly emitted + const eventFilter = this.token.filters.AmountDisclosed(); + const discloseEvent = (await this.token.queryFilter(eventFilter))[0]; + expect(discloseEvent.args[0]).to.equal(expectedHandle); + expect(discloseEvent.args[1]).to.equal(expectedAmount); + }); + }); + + describe('decimals', function () { + it('returns token decimals', async function () { + expect(await this.token.decimals()).to.equal(6); + }); + }); +}); + +function generateReencryptionErrorMessage(handle: string, account: string): string { + return `User ${account} is not authorized to user decrypt handle ${handle}`; +} diff --git a/protocol-contracts/confidential-wrapper/test/FeeManager.ts b/protocol-contracts/confidential-wrapper/test/FeeManager.ts new file mode 100644 index 0000000000..a14170ad66 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/FeeManager.ts @@ -0,0 +1,688 @@ +import { ethers } from "hardhat"; +import { FeeManager } from "../types"; +import { expect } from "chai"; +import { getSigners, Signers } from "./signers"; +import { deployFeeManagerFixture } from "./fixtures"; + +async function deployFixture(signers: Signers) { + const { feeManager, feeManagerAddress } = await deployFeeManagerFixture(signers.royalties); + + return { feeManager, feeManagerAddress }; +} + +describe("FeeManager", function () { + let signers: Signers; + let feeManager: FeeManager; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async () => { + ({ feeManager } = await deployFixture(signers)); + }); + + describe("Deployment", function () { + it("should deploy with correct admin role", async function () { + expect(await feeManager.hasRole(await feeManager.DEFAULT_ADMIN_ROLE(), signers.deployer.address)).to.be.true; + }); + + it("should set fees from constructor", async function () { + expect(await feeManager.wrapFeeBasisPoints()).to.equal(100); + expect(await feeManager.unwrapFeeBasisPoints()).to.equal(100); + }); + + it("should have set fee recipient from constructor", async function () { + expect(await feeManager.getFeeRecipient()).to.equal(signers.royalties.address); + }); + + it("should revert deployment with zero address as fee recipient", async function () { + const FeeManagerContract = await ethers.getContractFactory("FeeManager"); + + await expect( + FeeManagerContract.deploy( + 100, + 100, + ethers.parseEther("0.01"), + ethers.parseEther("0.001"), + ethers.ZeroAddress, + ) + ).to.be.revertedWithCustomError(FeeManagerContract, "ZeroAddressFeeRecipient"); + }); + + it("should revert deployment when wrapFeeBasisPoints exceeds 100%", async function () { + const FeeManagerContract = await ethers.getContractFactory("FeeManager"); + + await expect( + FeeManagerContract.deploy( + 10_001, // wrapFeeBasisPoints > 10_000 + 100, + ethers.parseEther("0.01"), + ethers.parseEther("0.001"), + signers.royalties.address, + ) + ).to.be.revertedWithCustomError(FeeManagerContract, "FeeExceedsMaximum"); + }); + + it("should revert deployment when unwrapFeeBasisPoints exceeds 100%", async function () { + const FeeManagerContract = await ethers.getContractFactory("FeeManager"); + + await expect( + FeeManagerContract.deploy( + 100, + 10_001, // unwrapFeeBasisPoints > 10_000 + ethers.parseEther("0.01"), + ethers.parseEther("0.001"), + signers.royalties.address, + ) + ).to.be.revertedWithCustomError(FeeManagerContract, "FeeExceedsMaximum"); + }); + + it("should allow deployment when both feeBasisPoints are exactly 100%", async function () { + const FeeManagerContract = await ethers.getContractFactory("FeeManager"); + + const feeManager = await FeeManagerContract.deploy( + 10_000, // wrapFeeBasisPoints = 100% + 10_000, // unwrapFeeBasisPoints = 100% + ethers.parseEther("0.01"), + ethers.parseEther("0.001"), + signers.royalties.address, + ); + + expect(await feeManager.wrapFeeBasisPoints()).to.equal(10_000); + expect(await feeManager.unwrapFeeBasisPoints()).to.equal(10_000); + }); + }); + + describe("Wrap/Unwrap Fee Configuration", function () { + it("should allow admin to set wrap fee basis points", async function () { + const feeBasisPoints = 50; // 0.5% + + await expect(feeManager.setWrapFeeBasisPoints(feeBasisPoints)) + .to.emit(feeManager, "WrapFeeBasisPointsUpdated") + .withArgs(100, feeBasisPoints); + + expect(await feeManager.wrapFeeBasisPoints()).to.equal(feeBasisPoints); + }); + + it("should allow admin to set unwrap fee basis points", async function () { + const feeBasisPoints = 75; // 0.75% + + await expect(feeManager.setUnwrapFeeBasisPoints(feeBasisPoints)) + .to.emit(feeManager, "UnwrapFeeBasisPointsUpdated") + .withArgs(100, feeBasisPoints); + + expect(await feeManager.unwrapFeeBasisPoints()).to.equal(feeBasisPoints); + }); + + it("should prevent setting wrap fee basis points above 100%", async function () { + await expect( + feeManager.setWrapFeeBasisPoints(10_001) + ).to.be.revertedWithCustomError(feeManager, "FeeExceedsMaximum"); + }); + + it("should prevent setting unwrap fee basis points above 100%", async function () { + await expect( + feeManager.setUnwrapFeeBasisPoints(10_001) + ).to.be.revertedWithCustomError(feeManager, "FeeExceedsMaximum"); + }); + + it("should prevent non-admin from setting wrap fee basis points", async function () { + await expect( + feeManager.connect(signers.alice).setWrapFeeBasisPoints(50) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + + it("should prevent non-admin from setting unwrap fee basis points", async function () { + await expect( + feeManager.connect(signers.alice).setUnwrapFeeBasisPoints(75) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Deploy Fee Configuration", function () { + it("should allow admin to set deploy fee", async function () { + const oldDeployFee = await feeManager.deployFee(); + const newDeployFee = oldDeployFee * BigInt(2); + + await expect(feeManager.setDeployFee(newDeployFee)) + .to.emit(feeManager, "DeployFeeUpdated") + .withArgs(oldDeployFee, newDeployFee); + + expect(await feeManager.deployFee()).to.equal(newDeployFee); + }); + + it("should allow setting deploy fee to zero", async function () { + // First set a non-zero fee + const initialFee = ethers.parseEther("0.05"); + await feeManager.setDeployFee(initialFee); + + // Then set it to zero + await expect(feeManager.setDeployFee(0)) + .to.emit(feeManager, "DeployFeeUpdated") + .withArgs(initialFee, 0); + + expect(await feeManager.deployFee()).to.equal(0); + }); + + it("should prevent non-admin from setting deploy fee", async function () { + await expect( + feeManager.connect(signers.alice).setDeployFee(ethers.parseEther("0.1")) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Fee Recipient Management", function () { + it("should allow admin to set fee recipient", async function () { + await expect(feeManager.setFeeRecipient(signers.alice.address)) + .to.emit(feeManager, "FeeRecipientUpdated") + .withArgs(signers.royalties.address, signers.alice.address); + + expect(await feeManager.getFeeRecipient()).to.equal(signers.alice.address); + }); + + it("should prevent setting zero address as fee recipient", async function () { + await expect( + feeManager.setFeeRecipient(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(feeManager, "ZeroAddressFeeRecipient"); + }); + + it("should prevent non-admin from setting fee recipient", async function () { + await expect( + feeManager.connect(signers.alice).setFeeRecipient(signers.royalties.address) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Wrap/Unwrap Fee Calculation", function () { + beforeEach(async () => { + await feeManager.setWrapFeeBasisPoints(100); // 1% + await feeManager.setUnwrapFeeBasisPoints(50); // 0.5% + }); + + for (const [amount, expectedWrapFee, expectedUnwrapFee] of [[0, 0, 0], [10_000, 100, 50]]) { + it(`should calculate wrap fee correctly (amount: ${amount})`, async function () { + expect(await feeManager.getWrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(expectedWrapFee); + }); + + it(`should calculate unwrap fee correctly (amount: ${amount})`, async function () { + const fee = await feeManager.getUnwrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + expect(fee).to.equal(expectedUnwrapFee); + }); + } + + it("should handle maximum values", async function () { + await feeManager.setWrapFeeBasisPoints(10_000); // 100% + const amount = 1000; + expect(await feeManager.getWrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(amount); + }); + }); + + describe("Fee Rounding (Ceiling Division)", function () { + it("should round up wrap fees to prevent fee leakage", async function () { + await feeManager.setWrapFeeBasisPoints(10); // 0.1% + + // Amount that would round down to 0 with floor division + // 99 * 10 / 10_000 = 0.099 -> floor = 0, ceil = 1 + expect(await feeManager.getWrapFee(99, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(1); + + // Amount that divides evenly should stay the same + // 10_000 * 10 / 10_000 = 10 + expect(await feeManager.getWrapFee(10_000, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(10); + + // Amount with small remainder should round up + // 10_001 * 10 / 10_000 = 10.001 -> floor = 10, ceil = 11 + expect(await feeManager.getWrapFee(10_001, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(11); + }); + + it("should round up unwrap fees to prevent fee leakage", async function () { + await feeManager.setUnwrapFeeBasisPoints(10); // 0.1% + + // Amount that would round down to 0 with floor division + // 99 * 10 / 10_000 = 0.099 -> floor = 0, ceil = 1 + expect(await feeManager.getUnwrapFee(99, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(1); + + // Amount that divides evenly should stay the same + // 10_000 * 10 / 10_000 = 10 + expect(await feeManager.getUnwrapFee(10_000, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(10); + + // Amount with small remainder should round up + // 10_001 * 10 / 10_000 = 10.001 -> floor = 10, ceil = 11 + expect(await feeManager.getUnwrapFee(10_001, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(11); + }); + + it("should prevent WBTC-like fee bypass attack through transaction splitting", async function () { + // Simulate cWBTC scenario from the vulnerability description: + // - WBTC has 8 decimals, cWBTC has 6 decimals (euint64 limitation) + // - 0.1 cWBTC = 100,000 units (in 6 decimals) + // - At 100k USD per BTC, this is 10k USD + // - With 0.1% fee (10 basis points), expected fee is ~10 USD or ~100 units + + await feeManager.setUnwrapFeeBasisPoints(10); // 0.1% + + const totalAmount = 100_000; // 0.1 cWBTC + const expectedTotalFee = 100; // 100,000 * 10 / 10_000 = 100 units + + // Calculate fee for the full amount + const feeFullAmount = await feeManager.getUnwrapFee(totalAmount, ethers.ZeroAddress, ethers.ZeroAddress); + expect(feeFullAmount).to.equal(expectedTotalFee); + + // Try to bypass fees by splitting into 101 chunks + // With floor division: 990 * 10 / 10_000 = 0.99 -> 0 fee per chunk + // With ceil division: 990 * 10 / 10_000 = 0.99 -> 1 fee per chunk + const chunkSize = 990; + const numChunks = 101; + const feePerChunk = await feeManager.getUnwrapFee(chunkSize, ethers.ZeroAddress, ethers.ZeroAddress); + + // With ceiling division, each chunk should pay at least 1 unit fee + expect(feePerChunk).to.equal(1); + + // Total fees from splitting should be >= original fee + const totalFeesFromSplitting = feePerChunk * BigInt(numChunks); + expect(totalFeesFromSplitting).to.be.gte(expectedTotalFee); + }); + + it("should round up swapper wrap fees when waiver is active", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, signers.alice.address); + await feeManager.setSwapperWrapFeeBasisPoints(10); // 0.1% + await feeManager.setSwapperFeeWaiverActive(true); + + // Amount that would round down to 0 with floor division + expect(await feeManager.getWrapFee(99, signers.alice.address, ethers.ZeroAddress)).to.equal(1); + }); + + it("should round up swapper unwrap fees when waiver is active", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, signers.alice.address); + await feeManager.setSwapperUnwrapFeeBasisPoints(10); // 0.1% + await feeManager.setSwapperFeeWaiverActive(true); + + // Amount that would round down to 0 with floor division + expect(await feeManager.getUnwrapFee(99, ethers.ZeroAddress, signers.alice.address)).to.equal(1); + }); + + it("should return 0 fee when amount is 0", async function () { + await feeManager.setWrapFeeBasisPoints(100); // 1% + await feeManager.setUnwrapFeeBasisPoints(100); // 1% + + expect(await feeManager.getWrapFee(0, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(0); + expect(await feeManager.getUnwrapFee(0, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(0); + }); + + it("should return 0 fee when fee basis points is 0", async function () { + await feeManager.setWrapFeeBasisPoints(0); + await feeManager.setUnwrapFeeBasisPoints(0); + + expect(await feeManager.getWrapFee(10_000, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(0); + expect(await feeManager.getUnwrapFee(10_000, ethers.ZeroAddress, ethers.ZeroAddress)).to.equal(0); + }); + }); + + describe("Access Control", function () { + it("should allow admin to grant admin role to another account", async function () { + const defaultAdminRole = await feeManager.DEFAULT_ADMIN_ROLE(); + + await feeManager.grantRole(defaultAdminRole, signers.alice.address); + expect(await feeManager.hasRole(defaultAdminRole, signers.alice.address)).to.be.true; + }); + + it("should allow admin to revoke admin role from another account", async function () { + const defaultAdminRole = await feeManager.DEFAULT_ADMIN_ROLE(); + + // Grant role first + await feeManager.grantRole(defaultAdminRole, signers.alice.address); + expect(await feeManager.hasRole(defaultAdminRole, signers.alice.address)).to.be.true; + + // Revoke role + await feeManager.revokeRole(defaultAdminRole, signers.alice.address); + expect(await feeManager.hasRole(defaultAdminRole, signers.alice.address)).to.be.false; + }); + + it("should prevent non-admin from granting roles", async function () { + const defaultAdminRole = await feeManager.DEFAULT_ADMIN_ROLE(); + + await expect( + feeManager.connect(signers.alice).grantRole(defaultAdminRole, signers.bob.address) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Role Separation - FEE_MANAGER_ROLE", function () { + it("should have FEE_MANAGER_ROLE constant defined", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + expect(feeManagerRole).to.equal(ethers.keccak256(ethers.toUtf8Bytes("FEE_MANAGER_ROLE"))); + }); + + it("should grant FEE_MANAGER_ROLE to deployer in fixture", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + expect(await feeManager.hasRole(feeManagerRole, signers.deployer.address)).to.be.true; + }); + + it("should allow DEFAULT_ADMIN to grant FEE_MANAGER_ROLE to another account", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + await feeManager.grantRole(feeManagerRole, signers.bob.address); + expect(await feeManager.hasRole(feeManagerRole, signers.bob.address)).to.be.true; + }); + + it("should allow FEE_MANAGER_ROLE to set wrap fee basis points", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // Grant FEE_MANAGER_ROLE to alice + await feeManager.grantRole(feeManagerRole, signers.alice.address); + + // Alice should be able to set wrap fee + await expect(feeManager.connect(signers.alice).setWrapFeeBasisPoints(200)) + .to.emit(feeManager, "WrapFeeBasisPointsUpdated") + .withArgs(100, 200); + + expect(await feeManager.wrapFeeBasisPoints()).to.equal(200); + }); + + it("should allow FEE_MANAGER_ROLE to set all fee parameters", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // Grant FEE_MANAGER_ROLE to alice + await feeManager.grantRole(feeManagerRole, signers.alice.address); + + // Test all fee setter functions + await expect(feeManager.connect(signers.alice).setWrapFeeBasisPoints(200)) + .to.not.be.reverted; + await expect(feeManager.connect(signers.alice).setUnwrapFeeBasisPoints(150)) + .to.not.be.reverted; + await expect(feeManager.connect(signers.alice).setDeployFee(ethers.parseEther("0.02"))) + .to.not.be.reverted; + await expect(feeManager.connect(signers.alice).setBatchTransferFee(ethers.parseEther("0.002"))) + .to.not.be.reverted; + await expect(feeManager.connect(signers.alice).setFeeRecipient(signers.bob.address)) + .to.not.be.reverted; + await expect(feeManager.connect(signers.alice).setSwapperFeeWaiverActive(true)) + .to.not.be.reverted; + }); + + it("should prevent accounts without FEE_MANAGER_ROLE from setting fees", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // alice does not have FEE_MANAGER_ROLE + await expect( + feeManager.connect(signers.alice).setWrapFeeBasisPoints(200) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount") + .withArgs(signers.alice.address, feeManagerRole); + }); + + it("should prevent DEFAULT_ADMIN without FEE_MANAGER_ROLE from setting fees", async function () { + const defaultAdminRole = await feeManager.DEFAULT_ADMIN_ROLE(); + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // Grant DEFAULT_ADMIN to bob but NOT FEE_MANAGER_ROLE + await feeManager.grantRole(defaultAdminRole, signers.bob.address); + + // Bob has DEFAULT_ADMIN but should NOT be able to set fees without FEE_MANAGER_ROLE + await expect( + feeManager.connect(signers.bob).setWrapFeeBasisPoints(200) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount") + .withArgs(signers.bob.address, feeManagerRole); + }); + + it("should allow DEFAULT_ADMIN to revoke FEE_MANAGER_ROLE", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // Grant FEE_MANAGER_ROLE to alice + await feeManager.grantRole(feeManagerRole, signers.alice.address); + expect(await feeManager.hasRole(feeManagerRole, signers.alice.address)).to.be.true; + + // Alice can set fees + await expect(feeManager.connect(signers.alice).setWrapFeeBasisPoints(200)) + .to.not.be.reverted; + + // Revoke the role + await feeManager.revokeRole(feeManagerRole, signers.alice.address); + expect(await feeManager.hasRole(feeManagerRole, signers.alice.address)).to.be.false; + + // Alice can no longer set fees + await expect( + feeManager.connect(signers.alice).setWrapFeeBasisPoints(300) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + + it("should isolate role management from fee operations", async function () { + const defaultAdminRole = await feeManager.DEFAULT_ADMIN_ROLE(); + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + + // Alice gets FEE_MANAGER_ROLE (can set fees) + await feeManager.grantRole(feeManagerRole, signers.alice.address); + + // Bob gets DEFAULT_ADMIN (can manage roles) + await feeManager.grantRole(defaultAdminRole, signers.bob.address); + + // Alice can set fees but NOT grant roles + await expect(feeManager.connect(signers.alice).setWrapFeeBasisPoints(200)) + .to.not.be.reverted; + await expect( + feeManager.connect(signers.alice).grantRole(feeManagerRole, signers.charlie.address) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + + // Bob can grant roles but NOT set fees + await expect(feeManager.connect(signers.bob).grantRole(feeManagerRole, signers.charlie.address)) + .to.not.be.reverted; + await expect( + feeManager.connect(signers.bob).setWrapFeeBasisPoints(300) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Swapper Fee Configuration", function () { + it("should initialize swapper fees to 0", async function () { + expect(await feeManager.swapperWrapFeeBasisPoints()).to.equal(0); + expect(await feeManager.swapperUnwrapFeeBasisPoints()).to.equal(0); + }); + + it("should allow admin to set swapper wrap fee basis points", async function () { + const feeBasisPoints = 25; // 0.25% + + await expect(feeManager.setSwapperWrapFeeBasisPoints(feeBasisPoints)) + .to.emit(feeManager, "SwapperWrapFeeBasisPointsUpdated") + .withArgs(0, feeBasisPoints); + + expect(await feeManager.swapperWrapFeeBasisPoints()).to.equal(feeBasisPoints); + }); + + it("should allow admin to set swapper unwrap fee basis points", async function () { + const feeBasisPoints = 30; // 0.30% + + await expect(feeManager.setSwapperUnwrapFeeBasisPoints(feeBasisPoints)) + .to.emit(feeManager, "SwapperUnwrapFeeBasisPointsUpdated") + .withArgs(0, feeBasisPoints); + + expect(await feeManager.swapperUnwrapFeeBasisPoints()).to.equal(feeBasisPoints); + }); + + it("should prevent setting swapper wrap fee basis points above 100%", async function () { + await expect( + feeManager.setSwapperWrapFeeBasisPoints(10_001) + ).to.be.revertedWithCustomError(feeManager, "FeeExceedsMaximum"); + }); + + it("should prevent setting swapper unwrap fee basis points above 100%", async function () { + await expect( + feeManager.setSwapperUnwrapFeeBasisPoints(10_001) + ).to.be.revertedWithCustomError(feeManager, "FeeExceedsMaximum"); + }); + + it("should prevent non-admin from setting swapper wrap fee basis points", async function () { + await expect( + feeManager.connect(signers.alice).setSwapperWrapFeeBasisPoints(25) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + + it("should prevent non-admin from setting swapper unwrap fee basis points", async function () { + await expect( + feeManager.connect(signers.alice).setSwapperUnwrapFeeBasisPoints(30) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + + it("should allow FEE_MANAGER_ROLE to set swapper fee basis points", async function () { + const feeManagerRole = await feeManager.FEE_MANAGER_ROLE(); + await feeManager.grantRole(feeManagerRole, signers.alice.address); + + await expect(feeManager.connect(signers.alice).setSwapperWrapFeeBasisPoints(50)) + .to.emit(feeManager, "SwapperWrapFeeBasisPointsUpdated") + .withArgs(0, 50); + + await expect(feeManager.connect(signers.alice).setSwapperUnwrapFeeBasisPoints(40)) + .to.emit(feeManager, "SwapperUnwrapFeeBasisPointsUpdated") + .withArgs(0, 40); + + expect(await feeManager.swapperWrapFeeBasisPoints()).to.equal(50); + expect(await feeManager.swapperUnwrapFeeBasisPoints()).to.equal(40); + }); + }); + + describe("Swapper Fee Waiver", function () { + it("should allow admin to activate swapper fee waiver", async function () { + await expect(feeManager.setSwapperFeeWaiverActive(true)) + .to.emit(feeManager, "SwapperFeeWaiverUpdated") + .withArgs(true); + + expect(await feeManager.swapperFeeWaiverActive()).to.be.true; + }); + + it("should allow admin to deactivate swapper fee waiver", async function () { + await feeManager.setSwapperFeeWaiverActive(true); + + await expect(feeManager.setSwapperFeeWaiverActive(false)) + .to.emit(feeManager, "SwapperFeeWaiverUpdated") + .withArgs(false); + + expect(await feeManager.swapperFeeWaiverActive()).to.be.false; + }); + + it("should prevent non-admin from changing swapper fee waiver", async function () { + await expect( + feeManager.connect(signers.alice).setSwapperFeeWaiverActive(true) + ).to.be.revertedWithCustomError(feeManager, "AccessControlUnauthorizedAccount"); + }); + + it("should charge swapper-specific wrap fees for SWAPPER role when waiver is active", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to alice + await feeManager.grantRole(swapperRole, signers.alice.address); + + // Set fee basis points + await feeManager.setWrapFeeBasisPoints(100); // 1% for regular users + await feeManager.setSwapperWrapFeeBasisPoints(25); // 0.25% for swappers + + // Activate swapper fee waiver + await feeManager.setSwapperFeeWaiverActive(true); + + // Test wrap fee for swapper should be swapperWrapFeeBasisPoints (25 basis points = 0.25%) + expect(await feeManager.getWrapFee(10_000, signers.alice.address, ethers.ZeroAddress)).to.equal(25); + + // Test wrap fee for non-swapper should still use standard fee + expect(await feeManager.getWrapFee(10_000, signers.bob.address, ethers.ZeroAddress)).to.equal(100); + }); + + it("should charge zero wrap fees for SWAPPER role when waiver is active and swapper fee is 0", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to alice + await feeManager.grantRole(swapperRole, signers.alice.address); + + // Set fee basis points + await feeManager.setWrapFeeBasisPoints(100); // 1% for regular users + // swapperWrapFeeBasisPoints is 0 by default + + // Activate swapper fee waiver + await feeManager.setSwapperFeeWaiverActive(true); + + // Test wrap fee for swapper should be 0 (since swapperWrapFeeBasisPoints = 0) + expect(await feeManager.getWrapFee(10_000, signers.alice.address, ethers.ZeroAddress)).to.equal(0); + + // Test wrap fee for non-swapper should still apply + expect(await feeManager.getWrapFee(10_000, signers.bob.address, ethers.ZeroAddress)).to.equal(100); + }); + + it("should charge wrap fees for SWAPPER role when waiver is inactive", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to alice + await feeManager.grantRole(swapperRole, signers.alice.address); + + // Set fee basis points to non-zero + await feeManager.setWrapFeeBasisPoints(100); // 1% + await feeManager.setSwapperWrapFeeBasisPoints(25); // 0.25% for swappers + + // Keep swapper fee waiver inactive (default) + expect(await feeManager.swapperFeeWaiverActive()).to.be.false; + + // Test wrap fee for swapper should use standard fee when waiver is inactive + expect(await feeManager.getWrapFee(10_000, signers.alice.address, ethers.ZeroAddress)).to.equal(100); + }); + + it("should charge swapper-specific unwrap fees for SWAPPER role when waiver is active", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to alice + await feeManager.grantRole(swapperRole, signers.alice.address); + + // Set fee basis points + await feeManager.setUnwrapFeeBasisPoints(50); // 0.5% for regular users + await feeManager.setSwapperUnwrapFeeBasisPoints(10); // 0.1% for swappers + + // Activate swapper fee waiver + await feeManager.setSwapperFeeWaiverActive(true); + + const amount = 10_000; + + // Test unwrap fee for swapper destination should be swapperUnwrapFeeBasisPoints (10 basis points = 0.1%) + const swapperFee = await feeManager.getUnwrapFee( + amount, + ethers.ZeroAddress, + signers.alice.address // unwrapTo is the swapper + ); + expect(swapperFee).to.equal(10); + + // Test unwrap fee for non-swapper destination should still apply standard fee + const nonSwapperFee = await feeManager.getUnwrapFee( + amount, + ethers.ZeroAddress, + signers.bob.address // unwrapTo is not the swapper + ); + expect(nonSwapperFee).to.equal(50); // 0.5% of 10,000 + }); + + it("should charge zero unwrap fees for SWAPPER role when waiver is active and swapper fee is 0", async function () { + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to alice + await feeManager.grantRole(swapperRole, signers.alice.address); + + // Set fee basis points + await feeManager.setUnwrapFeeBasisPoints(50); // 0.5% for regular users + // swapperUnwrapFeeBasisPoints is 0 by default + + // Activate swapper fee waiver + await feeManager.setSwapperFeeWaiverActive(true); + + const amount = 10_000; + + // Test unwrap fee for swapper destination should be 0 (since swapperUnwrapFeeBasisPoints = 0) + const swapperFee = await feeManager.getUnwrapFee( + amount, + ethers.ZeroAddress, + signers.alice.address // unwrapTo is the swapper + ); + expect(swapperFee).to.equal(0); + + // Test unwrap fee for non-swapper destination should still apply + const nonSwapperFee = await feeManager.getUnwrapFee( + amount, + ethers.ZeroAddress, + signers.bob.address // unwrapTo is not the swapper + ); + expect(nonSwapperFee).to.equal(50); // 0.5% of 10,000 + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/RegulatedERC7984Upgradeable.ts b/protocol-contracts/confidential-wrapper/test/RegulatedERC7984Upgradeable.ts new file mode 100644 index 0000000000..97047722c5 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/RegulatedERC7984Upgradeable.ts @@ -0,0 +1,1149 @@ +import { ethers, fhevm, upgrades } from "hardhat"; +import { RegulatedERC7984Upgradeable, AdminProvider } from "../types"; +import { expect } from "chai"; +import { getMintEvent, getBurnEvent, getTransferInfoEvent, getConfidentialBalance, getTransferFeeInfoEvent, checkTotalSupply, getTokenRegulatorUpdatedEvent } from "./utils"; +import { getSigners, Signers } from "./signers"; +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { deployAdminProviderFixture, deployBurnableConfidentialErc20Fixture, deployConfidentialErc20Fixture, deploySanctionsListFixture } from "./fixtures"; + + +describe("RegulatedERC7984Upgradeable", function () { + let signers: Signers; + let adminProvider: AdminProvider; + let adminProviderAddress: string; + let cErc20: RegulatedERC7984Upgradeable; + let cErc20Address: string; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async () => { + ({ cErc20, cErc20Address, adminProvider, adminProviderAddress } = await deployConfidentialErc20Fixture(signers)); + await cErc20.grantRole(await cErc20.WRAPPER_ROLE(), signers.deployer); + }); + + describe("initializer", function () { + it("should set correct attribute values at initialization", async function () { + expect(await cErc20.name()).to.equal("Naraggara"); + expect(await cErc20.symbol()).to.equal("NARA"); + expect(await cErc20.decimals()).to.equal(6); + expect(await cErc20.rate()).to.equal(1); + expect(await cErc20.adminProvider()).to.equal(adminProvider); + + + const totalSupplyHandle = await cErc20.confidentialTotalSupply(); + + await checkTotalSupply(cErc20, 0); + }); + + it("should set the correct admin role", async function () { + expect(await cErc20.hasRole(await cErc20.DEFAULT_ADMIN_ROLE(), signers.deployer)).to.equal(true); + }); + + it("cannot call initialize twice", async function () { + await expect( + cErc20.initialize("Naraggara", "NARA", 6, signers.deployer.address, 1, ethers.ZeroAddress, adminProviderAddress, signers.deployer.address), + ).to.be.revertedWithCustomError(cErc20, "InvalidInitialization"); + }); + + it("should revert when rate is 0", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const initializerParams = [ + "Test Token", + "TEST", + 6, + signers.deployer.address, + 0, // Invalid rate: 0 + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ]; + await expect( + upgrades.deployProxy(RegulatedERC7984Factory, initializerParams) + ).to.be.revertedWithCustomError(RegulatedERC7984Factory, "InvalidRate"); + }); + + it("should revert when rate exceeds maximum (10^24)", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const maxRate = BigInt(10) ** BigInt(24); + const invalidRate = maxRate + BigInt(1); + const initializerParams = [ + "Test Token", + "TEST", + 30, + signers.deployer.address, + invalidRate, // Invalid rate: exceeds 10^24 + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ]; + await expect( + upgrades.deployProxy(RegulatedERC7984Factory, initializerParams) + ).to.be.revertedWithCustomError(RegulatedERC7984Factory, "InvalidRate"); + }); + + it("should accept rate of 1 (tokens with <= 6 decimals)", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const initializerParams = [ + "USDC", + "USDC", + 6, + signers.deployer.address, + 1, // Valid rate for 6 decimals + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams) + await newCErc20.waitForDeployment(); + expect(await newCErc20.rate()).to.equal(1); + }); + + it("should accept rate of 10^12 (18 decimal tokens)", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const rate = BigInt(10) ** BigInt(12); // 18 decimals - 6 = 12 + const initializerParams = [ + "Ethereum", + "ETH", + 6, // Confidential token has 6 decimals + signers.deployer.address, + rate, // Valid rate for 18 decimal original token + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams) + await newCErc20.waitForDeployment(); + expect(await newCErc20.rate()).to.equal(rate); + }); + + it("should accept maximum rate of 10^24 (30 decimal tokens)", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const maxRate = BigInt(10) ** BigInt(24); // 30 decimals - 6 = 24 + const rate = BigInt(10) ** BigInt(12); // 18 decimals - 6 = 12 + const initializerParams = [ + "High Decimal Token", + "HDT", + 6, // Confidential token has 6 decimals + signers.deployer.address, + maxRate, // Maximum valid rate for 30 decimal original token + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams) + await newCErc20.waitForDeployment(); + expect(await newCErc20.rate()).to.equal(maxRate); + }); + + it("should prevent implementation contract from initializing", async function () { + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const newCErc20 = await RegulatedERC7984Factory.deploy(); + await newCErc20.waitForDeployment(); + await expect( + newCErc20.initialize( + "High Decimal Token", + "HDT", + 6, + signers.deployer.address, + 1, + ethers.ZeroAddress, + adminProviderAddress, + signers.deployer.address // wrapperSetter + ) + ).to.be.revertedWithCustomError(RegulatedERC7984Factory, "InvalidInitialization"); + }); + }) + + describe("setWrapper", function () { + it("should allow WRAPPER_SETTER_ROLE to set wrapper once", async function () { + // Deploy a fresh token without wrapper set + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const initializerParams = [ + "Test Token", + "TEST", + 6, + signers.deployer.address, // admin + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.alice.address // alice gets WRAPPER_SETTER_ROLE + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams); + await newCErc20.waitForDeployment(); + + // Alice (WRAPPER_SETTER_ROLE) should be able to set wrapper + await newCErc20.connect(signers.alice).setWrapper(signers.bob.address); + + // Verify bob now has WRAPPER_ROLE + expect(await newCErc20.hasRole(await newCErc20.WRAPPER_ROLE(), signers.bob.address)).to.equal(true); + }); + + it("should revert with WrapperAlreadySet when trying to set wrapper twice", async function () { + // Deploy a fresh token without wrapper set + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const initializerParams = [ + "Test Token", + "TEST", + 6, + signers.deployer.address, // admin + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.alice.address // alice gets WRAPPER_SETTER_ROLE + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams); + await newCErc20.waitForDeployment(); + + // Alice sets wrapper first time + await newCErc20.connect(signers.alice).setWrapper(signers.bob.address); + + // Try to set wrapper second time - should revert + await expect( + newCErc20.connect(signers.alice).setWrapper(signers.charlie.address) + ).to.be.revertedWithCustomError(newCErc20, "WrapperAlreadySet"); + }); + + it("should prevent non-WRAPPER_SETTER_ROLE from calling setWrapper", async function () { + // Deploy a fresh token without wrapper set + const RegulatedERC7984Factory = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const initializerParams = [ + "Test Token", + "TEST", + 6, + signers.deployer.address, // admin + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.alice.address // only alice gets WRAPPER_SETTER_ROLE + ]; + const newCErc20 = await upgrades.deployProxy(RegulatedERC7984Factory, initializerParams); + await newCErc20.waitForDeployment(); + + // Bob (without WRAPPER_SETTER_ROLE) should not be able to set wrapper + const WRAPPER_SETTER_ROLE = await newCErc20.WRAPPER_SETTER_ROLE(); + await expect( + newCErc20.connect(signers.bob).setWrapper(signers.charlie.address) + ).to.be.revertedWithCustomError(newCErc20, "AccessControlUnauthorizedAccount") + .withArgs(signers.bob.address, WRAPPER_SETTER_ROLE); + }); + }); + + describe("mint", function () { + it("should mint correct amount and emit event", async function () { + const amount = 1000; + const txId = await cErc20.nextTxId(); + const isRoyalty = true; + const transaction = await cErc20.mint(signers.alice, amount); + const receipt = await transaction.wait(); + + await checkTotalSupply(cErc20, amount); + + const events = getMintEvent(receipt); + + expect(events.length).to.be.equal(1); + const event = events[0]; + expect(event.args[0]).to.be.equal(signers.alice); + expect(event.args[1]).to.be.equal(amount); + expect(event.args[2]).to.be.equal(txId); + + // Reencrypt Alice's balance from Alice + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAliceFromAlice = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.alice, + ); + + expect(balanceAliceFromAlice).to.equal(amount); + + // Reencrypt Alice's balance from regulator + const balanceAliceFromAdmin = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.regulator, + ); + + expect(balanceAliceFromAdmin).to.equal(amount); + }); + + it("should prevent minting to sanctioned address", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20 } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Add alice to sanctions list + await sanctionsList.addToSanctionsList([signers.alice.address]); + + // Try to mint to sanctioned address + await expect( + cErc20.mint(signers.alice.address, 1000) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.alice.address); + }); + + it("should allow minting to non-sanctioned address", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20 } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Sanction someone else, not alice + await sanctionsList.addToSanctionsList([signers.bob.address]); + + // Should be able to mint to alice (not sanctioned) + await expect( + cErc20.mint(signers.alice.address, 1000) + ).to.not.be.reverted; + }); + + it("should work without sanctions list", async function () { + // Use the regular fixture without sanctions list + await expect( + cErc20.mint(signers.alice.address, 1000) + ).to.not.be.reverted; + }); + + it("should only allow WRAPPER_ROLE to mint tokens", async function () { + await expect( + cErc20.connect(signers.alice).mint(signers.alice, 1000) + ).to.be.revertedWithCustomError(cErc20, "AccessControlUnauthorizedAccount") + .withArgs(signers.alice.address, await cErc20.WRAPPER_ROLE()); + }); + }); + + describe("burn", function () { + it("should burn correct amount and emit event", async function () { + const { burnableCErc20, burnableCErc20Address } = await deployBurnableConfidentialErc20Fixture(signers); + + const amount = 1000; + const txId = await burnableCErc20.nextTxId(); + const isRoyalty = false; + + await burnableCErc20.mint(signers.deployer, amount); + + const encryptedAmount = await fhevm + .createEncryptedInput(burnableCErc20Address, signers.deployer.address) + .add64(amount) + .encrypt(); + + const transaction = await burnableCErc20 + .connect(signers.deployer) + ["burn(bytes32,bytes)"] + (encryptedAmount.handles[0], encryptedAmount.inputProof); + const receipt = await transaction.wait(); + + await checkTotalSupply(cErc20, 0); + + const events = getBurnEvent(receipt); + expect(events.length).to.be.equal(1); + const burnEvent = events[0]; + expect(burnEvent.args[0]).to.equal(signers.deployer); + expect(burnEvent.args[2]).to.equal(txId + BigInt(1)); + const burnAmountHandle = burnEvent.args[1]; + + const burnAmount = await fhevm.userDecryptEuint( + FhevmType.euint64, + burnAmountHandle, + burnableCErc20Address, + signers.deployer, + ); + expect(burnAmount).to.be.equal(amount); + + expect(await getConfidentialBalance(burnableCErc20, signers.deployer)).to.equal(0); + }); + + it("should only allow WRAPPER_ROLE to burn tokens", async function () { + const { burnableCErc20, burnableCErc20Address } = await deployBurnableConfidentialErc20Fixture(signers); + + const encryptedAmount = await fhevm + .createEncryptedInput(burnableCErc20Address, signers.alice.address) + .add64(100) + .encrypt(); + + const transaction = burnableCErc20 + .connect(signers.alice) + ["burn(bytes32,bytes)"] + (encryptedAmount.handles[0], encryptedAmount.inputProof); + + await expect(transaction).to.be.revertedWithCustomError(cErc20, "AccessControlUnauthorizedAccount") + .withArgs(signers.alice.address, await cErc20.WRAPPER_ROLE()); + }); + }); + + describe("confidentialTransfer", function () { + it("should transfer tokens between two users without fees", async function () { + const aliceInitialAmount = 10_000; + const transaction = await cErc20.mint(signers.alice, aliceInitialAmount); + const t1 = await transaction.wait(); + expect(t1?.status).to.eq(1); + + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const txId = await cErc20.nextTxId(); + + const tx = await cErc20 + .connect(signers.alice) + [ + "confidentialTransfer(address,bytes32,bytes)" + ](signers.bob, encryptedTransferAmount.handles[0], encryptedTransferAmount.inputProof); + const t2 = await tx.wait(); + expect(t2?.status).to.eq(1); + + await checkTotalSupply(cErc20, aliceInitialAmount); + + // admin, alice & bob should be able to decrypt amount in event + const events = getTransferInfoEvent(t2); + expect(events.length).to.be.equal(1); + const event = events[0]; + expect(event.args[0]).to.be.equal(signers.alice); + expect(event.args[1]).to.be.equal(signers.bob); + expect(event.args[3]).to.be.equal(txId); + const transferAmountHandle = event.args[2]; + + // check reencrypt transfer amount handle + for (const signer of [signers.regulator, signers.alice, signers.bob]) { + const transferAmountFromSigner = await fhevm.userDecryptEuint( + FhevmType.euint64, + transferAmountHandle, + cErc20Address, + signer, + ); + expect(transferAmountFromSigner).to.equal(transferAmount); + } + + // charlie should NOT be able to decrypt amount in event + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, cErc20Address, signers.charlie), + ).to.be.rejectedWith( + `User ${signers.charlie.address} is not authorized to user decrypt handle ${transferAmountHandle}!`, + ); + + // Royalties balance is not initialized (ie is 0) + await expect( + getConfidentialBalance(cErc20, signers.royalties) + ).to.be.rejectedWith("Handle is not initialized"); + + // Reencrypt Alice's balance from Alice + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAlice = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.alice, + ); + expect(balanceAlice).to.equal(aliceInitialAmount - transferAmount); + + // Reencrypt Bob's balance from Bob + const balanceHandleBob = await cErc20.confidentialBalanceOf(signers.bob); + const balanceBob = await fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleBob, cErc20Address, signers.bob); + expect(balanceBob).to.equal(transferAmount); + + // on the other hand, Bob should be unable to read Alice's balance + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleAlice, cErc20Address, signers.bob), + ).to.be.rejectedWith( + `User ${signers.bob.address} is not authorized to user decrypt handle ${balanceHandleAlice}!`, + ); + + // and should be impossible to call reencrypt if contractAddress === userAddress + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleAlice, signers.alice.address, signers.alice), + ).to.be.rejectedWith( + `userAddress ${signers.alice.address} should not be equal to contractAddress when requesting decryption!`, + ); + + const transferFeeInfoEvents = getTransferFeeInfoEvent(t2); + expect(transferFeeInfoEvents.length).to.equal(0); + + expect(await cErc20.nextTxId()).to.equal(txId + BigInt(1)); + }); + + it("should not transfer tokens between two users when sending more than balance", async function () { + const transaction = await cErc20.mint(signers.alice, 1000); + await transaction.wait(); + + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const txId = await cErc20.nextTxId(); + + const tx = await cErc20 + .connect(signers.alice) + [ + "confidentialTransfer(address,bytes32,bytes)" + ](signers.bob, encryptedTransferAmount.handles[0], encryptedTransferAmount.inputProof); + const receipt = await tx.wait(); + + expect(await cErc20.nextTxId()).to.equal(txId + BigInt(1)); + + const events = getTransferInfoEvent(receipt); + expect(events.length).to.be.equal(1); + const transferEvent = events[0]; + expect(transferEvent.args[0]).to.be.equal(signers.alice); + expect(transferEvent.args[1]).to.be.equal(signers.bob); + expect(transferEvent.args[3]).to.be.equal(txId); + const transferAmountHandle = transferEvent.args[2]; + + // check reencrypt transfer amount handle + for (const signer of [signers.regulator, signers.alice, signers.bob]) { + const transferAmountFromSigner = await fhevm.userDecryptEuint( + FhevmType.euint64, + transferAmountHandle, + cErc20Address, + signer, + ); + expect(transferAmountFromSigner).to.equal(0); + } + + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAlice = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.alice, + ); + expect(balanceAlice).to.equal(1000); + + // Reencrypt Bob's balance + const balanceHandleBob = await cErc20.confidentialBalanceOf(signers.bob); + const balanceBob = await fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleBob, cErc20Address, signers.bob); + expect(balanceBob).to.equal(0); + }); + + it("should prevent transfer from sanctioned address", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Mint tokens to alice first + await cErc20.mint(signers.alice.address, 10000); + + const txId = await cErc20.nextTxId(); + + // Add alice to sanctions list + await sanctionsList.addToSanctionsList([signers.alice.address]); + + const transferAmount = 100; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Try to transfer from sanctioned address + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.alice.address); + + expect(await cErc20.nextTxId()).to.equal(txId); + }); + + it("should prevent transfer to sanctioned address", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Mint tokens to alice first + await cErc20.mint(signers.alice.address, 10000); + + // Add bob to sanctions list + await sanctionsList.addToSanctionsList([signers.bob.address]); + + const txId = await cErc20.nextTxId(); + + const transferAmount = 100; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Try to transfer to sanctioned address + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.bob.address); + + expect(await cErc20.nextTxId()).to.equal(txId); + }); + + it("should allow transfer when no sanctions are in place", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Mint tokens to alice first + await cErc20.mint(signers.alice.address, 10000); + + const txId = await cErc20.nextTxId(); + + // Sanction someone else, not alice or bob + await sanctionsList.addToSanctionsList([signers.charlie.address]); + + const transferAmount = 100; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Should work fine + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.not.be.reverted; + + expect(await cErc20.nextTxId()).to.equal(txId + BigInt(1)); + }); + + it("should allow dynamic sanctions list updates", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Mint tokens to alice first + await cErc20.mint(signers.alice.address, 10000); + + const transferAmount = 100; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Initially should work + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.not.be.reverted; + + // Add alice to sanctions list + await sanctionsList.addToSanctionsList([signers.alice.address]); + + const encryptedTransferAmount2 = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount2.handles[0], + encryptedTransferAmount2.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.alice.address); + + // Remove alice from sanctions list + await sanctionsList.removeFromSanctionsList([signers.alice.address]); + + const encryptedTransferAmount3 = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Should work again + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob.address, + encryptedTransferAmount3.handles[0], + encryptedTransferAmount3.inputProof + ) + ).to.not.be.reverted; + }); + + it("should prevent transfer to zero address", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + await cErc20.mint(signers.alice.address, 10000); + + const txId = await cErc20.nextTxId(); + + const transferAmount = 100; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + await expect( + cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + ethers.ZeroAddress, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "ERC7984InvalidReceiver"); + + expect(await cErc20.nextTxId()).to.equal(txId); + }); + }); + + describe("confidentialTransferFrom", function () { + it("should prevent transferFrom when from address is sanctioned", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Setup: mint tokens to alice and approve bob to spend them + const aliceInitialBalance = 10_000; + await cErc20.mint(signers.alice.address, aliceInitialBalance); + + const setOperator = await cErc20.connect(signers.alice).setOperator(signers.bob, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Sanction alice (the from address) + await sanctionsList.addToSanctionsList([signers.alice.address]); + + const transferAmount = 500; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + // Try transferFrom when from address is sanctioned + await expect( + cErc20 + .connect(signers.bob) + ["confidentialTransferFrom(address,address,bytes32,bytes)"]( + signers.alice.address, + signers.charlie.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.alice.address); + }); + + it("should prevent transferFrom when to address is sanctioned", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Setup: mint tokens to alice and approve bob to spend them + const aliceInitialBalance = 10_000; + await cErc20.mint(signers.alice.address, aliceInitialBalance); + + const setOperator = await cErc20.connect(signers.alice).setOperator(signers.bob, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Sanction charlie (the to address) + await sanctionsList.addToSanctionsList([signers.charlie.address]); + + const transferAmount = 500; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + // Try transferFrom when to address is sanctioned + await expect( + cErc20 + .connect(signers.bob) + ["confidentialTransferFrom(address,address,bytes32,bytes)"]( + signers.alice.address, + signers.charlie.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.charlie.address); + }); + + it("should prevent transferFrom when operator (msg.sender) is sanctioned", async function () { + const { sanctionsList } = await deploySanctionsListFixture(); + const { adminProvider } = await deployAdminProviderFixture(signers, sanctionsList); + const { cErc20, cErc20Address } = await deployConfidentialErc20Fixture(signers, adminProvider); + + // Setup: mint tokens to alice and approve bob to spend them + const aliceInitialBalance = 10_000; + await cErc20.mint(signers.alice.address, aliceInitialBalance); + + const setOperator = await cErc20.connect(signers.alice).setOperator(signers.bob, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Sanction bob (the operator/msg.sender) + await sanctionsList.addToSanctionsList([signers.bob.address]); + + const transferAmount = 500; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + // Try transferFrom when operator is sanctioned - should revert + await expect( + cErc20 + .connect(signers.bob) + ["confidentialTransferFrom(address,address,bytes32,bytes)"]( + signers.alice.address, + signers.charlie.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ) + ).to.be.revertedWithCustomError(cErc20, "SanctionedAddress") + .withArgs(signers.bob.address); + }); + + it("should revert if no operator", async function () { + const aliceInitialBalance = 10_000; + const transaction = await cErc20.mint(signers.alice, aliceInitialBalance); + await transaction.wait(); + + const encryptedTransferAmount1 = await fhevm + .createEncryptedInput(cErc20Address, signers.bob.address) + .add64(aliceInitialBalance) + .encrypt(); + + await expect( + cErc20.connect(signers.bob) + [ + "confidentialTransferFrom(address,address,bytes32,bytes)" + ](signers.alice, signers.bob, encryptedTransferAmount1.handles[0], encryptedTransferAmount1.inputProof) + ).to.be.revertedWithCustomError(cErc20, "ERC7984UnauthorizedSpender"); + + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAlice = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.alice, + ); + expect(balanceAlice).to.equal(aliceInitialBalance); + }); + }); + + describe("tokenRegulator", function () { + it("should initially be address(0)", async function () { + expect(await cErc20.tokenRegulator()).to.equal(ethers.ZeroAddress); + }); + + it("should allow admin to set tokenRegulator", async function () { + const newRegulator = signers.charlie.address; + const tx = await cErc20.setTokenRegulator(newRegulator); + const receipt = await tx.wait(); + + expect(await cErc20.tokenRegulator()).to.equal(newRegulator); + + // Check event emission using helper + const events = getTokenRegulatorUpdatedEvent(receipt); + expect(events.length).to.equal(1); + expect(events[0].args[0]).to.equal(ethers.ZeroAddress); // oldRegulator + expect(events[0].args[1]).to.equal(newRegulator); // newRegulator + }); + + it("should emit TokenRegulatorUpdated event when updating tokenRegulator", async function () { + const firstRegulator = signers.charlie.address; + await cErc20.setTokenRegulator(firstRegulator); + + const secondRegulator = signers.bob.address; + const tx = await cErc20.setTokenRegulator(secondRegulator); + const receipt = await tx.wait(); + + const events = getTokenRegulatorUpdatedEvent(receipt); + expect(events.length).to.equal(1); + expect(events[0].args[0]).to.equal(firstRegulator); // oldRegulator + expect(events[0].args[1]).to.equal(secondRegulator); // newRegulator + }); + + it("should revert when non-admin tries to set tokenRegulator", async function () { + await expect( + cErc20.connect(signers.alice).setTokenRegulator(signers.charlie.address) + ).to.be.revertedWithCustomError(cErc20, "AccessControlUnauthorizedAccount") + .withArgs(signers.alice.address, await cErc20.DEFAULT_ADMIN_ROLE()); + }); + + it("should allow tokenRegulator to decrypt balances after transfer when set", async function () { + // Set charlie as the token regulator + await cErc20.setTokenRegulator(signers.charlie.address); + + // Mint tokens to alice + const aliceInitialAmount = 10_000; + await cErc20.mint(signers.alice, aliceInitialAmount); + + // Transfer from alice to bob + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const tx = await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ); + await tx.wait(); + + // Token regulator (charlie) should be able to decrypt alice's balance + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAliceFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.charlie, + ); + expect(balanceAliceFromTokenRegulator).to.equal(aliceInitialAmount - transferAmount); + + // Token regulator (charlie) should be able to decrypt bob's balance + const balanceHandleBob = await cErc20.confidentialBalanceOf(signers.bob); + const balanceBobFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleBob, + cErc20Address, + signers.charlie, + ); + expect(balanceBobFromTokenRegulator).to.equal(transferAmount); + }); + + it("should allow tokenRegulator to decrypt transfer amount", async function () { + // Set charlie as the token regulator + await cErc20.setTokenRegulator(signers.charlie.address); + + // Mint tokens to alice + const aliceInitialAmount = 10_000; + await cErc20.mint(signers.alice, aliceInitialAmount); + + // Transfer from alice to bob + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const tx = await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ); + const receipt = await tx.wait(); + + // Get transfer event + const events = getTransferInfoEvent(receipt); + expect(events.length).to.be.equal(1); + const transferAmountHandle = events[0].args[2]; + + // Token regulator (charlie) should be able to decrypt transfer amount + const transferAmountFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + transferAmountHandle, + cErc20Address, + signers.charlie, + ); + expect(transferAmountFromTokenRegulator).to.equal(transferAmount); + }); + + it("should allow both adminProvider regulator and tokenRegulator to decrypt simultaneously", async function () { + // Set charlie as the token regulator + await cErc20.setTokenRegulator(signers.charlie.address); + + // Mint tokens to alice + const aliceInitialAmount = 10_000; + await cErc20.mint(signers.alice, aliceInitialAmount); + + // Transfer from alice to bob + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const tx = await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ); + const receipt = await tx.wait(); + + // Get transfer event + const events = getTransferInfoEvent(receipt); + const transferAmountHandle = events[0].args[2]; + + // Both the adminProvider regulator and tokenRegulator should be able to decrypt + const transferAmountFromAdminRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + transferAmountHandle, + cErc20Address, + signers.regulator, + ); + expect(transferAmountFromAdminRegulator).to.equal(transferAmount); + + const transferAmountFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + transferAmountHandle, + cErc20Address, + signers.charlie, + ); + expect(transferAmountFromTokenRegulator).to.equal(transferAmount); + + // Both should be able to decrypt balances too + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + + const balanceAliceFromAdminRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.regulator, + ); + expect(balanceAliceFromAdminRegulator).to.equal(aliceInitialAmount - transferAmount); + + const balanceAliceFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.charlie, + ); + expect(balanceAliceFromTokenRegulator).to.equal(aliceInitialAmount - transferAmount); + }); + + it("should not allow tokenRegulator to decrypt when set to address(0)", async function () { + // Mint tokens to alice + const aliceInitialAmount = 10_000; + await cErc20.mint(signers.alice, aliceInitialAmount); + + // Transfer from alice to bob (with tokenRegulator still at address(0)) + const transferAmount = 1337; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const tx = await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof + ); + await tx.wait(); + + // Charlie should NOT be able to decrypt alice's balance (not set as regulator) + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleAlice, cErc20Address, signers.charlie), + ).to.be.rejectedWith( + `User ${signers.charlie.address} is not authorized to user decrypt handle ${balanceHandleAlice}!`, + ); + }); + + it("should revoke tokenRegulator access when set back to address(0)", async function () { + // Set charlie as the token regulator + await cErc20.setTokenRegulator(signers.charlie.address); + + // Mint tokens to alice + const aliceInitialAmount = 10_000; + await cErc20.mint(signers.alice, aliceInitialAmount); + + // First transfer - charlie should be able to decrypt + const transferAmount1 = 1000; + const encryptedTransferAmount1 = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount1) + .encrypt(); + + await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount1.handles[0], + encryptedTransferAmount1.inputProof + ); + + // Verify charlie can decrypt + const balanceHandleAlice1 = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAlice1 = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice1, + cErc20Address, + signers.charlie, + ); + expect(balanceAlice1).to.equal(aliceInitialAmount - transferAmount1); + + // Now revoke tokenRegulator by setting to address(0) + await cErc20.setTokenRegulator(ethers.ZeroAddress); + + // Second transfer - charlie should NOT be able to decrypt new balances + const transferAmount2 = 500; + const encryptedTransferAmount2 = await fhevm + .createEncryptedInput(cErc20Address, signers.alice.address) + .add64(transferAmount2) + .encrypt(); + + await cErc20 + .connect(signers.alice) + ["confidentialTransfer(address,bytes32,bytes)"]( + signers.bob, + encryptedTransferAmount2.handles[0], + encryptedTransferAmount2.inputProof + ); + + // Charlie should NOT be able to decrypt the new balance + const balanceHandleAlice2 = await cErc20.confidentialBalanceOf(signers.alice); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceHandleAlice2, cErc20Address, signers.charlie), + ).to.be.rejectedWith( + `User ${signers.charlie.address} is not authorized to user decrypt handle ${balanceHandleAlice2}!`, + ); + }); + + it("should allow tokenRegulator to decrypt mint event balances", async function () { + // Set charlie as the token regulator + await cErc20.setTokenRegulator(signers.charlie.address); + + // Mint tokens to alice + const amount = 1000; + await cErc20.mint(signers.alice, amount); + + // Token regulator (charlie) should be able to decrypt alice's balance + const balanceHandleAlice = await cErc20.confidentialBalanceOf(signers.alice); + const balanceAliceFromTokenRegulator = await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleAlice, + cErc20Address, + signers.charlie, + ); + expect(balanceAliceFromTokenRegulator).to.equal(amount); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/RegulatedERC7984UpgradeableFactory.ts b/protocol-contracts/confidential-wrapper/test/RegulatedERC7984UpgradeableFactory.ts new file mode 100644 index 0000000000..7bc984f486 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/RegulatedERC7984UpgradeableFactory.ts @@ -0,0 +1,394 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { getSigners, Signers } from "./signers"; +import { RegulatedERC7984UpgradeableFactory, AdminProvider, RegulatedERC7984Upgradeable, RegulatedERC7984UpgradeableV2Mock } from "../types"; +import { deployConfidentialTokenFactoryFixture, deployAdminProviderFixture } from "./fixtures"; + +describe("RegulatedERC7984UpgradeableFactory", function () { + let signers: Signers; + let confidentialTokenFactory: RegulatedERC7984UpgradeableFactory; + let adminProvider: AdminProvider; + let implementation: string; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + const { confidentialTokenFactory: deployedFactory } = await deployConfidentialTokenFactoryFixture(); + confidentialTokenFactory = deployedFactory; + + const { adminProvider: deployedProvider } = await deployAdminProviderFixture(signers); + adminProvider = deployedProvider; + + // Deploy canonical implementation for testing + const RegulatedERC7984Impl = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const implContract = await RegulatedERC7984Impl.deploy(); + await implContract.waitForDeployment(); + implementation = await implContract.getAddress(); + }); + + describe("deployConfidentialToken", function () { + it("should prevent non-owner from calling deployConfidentialToken", async function () { + await expect( + confidentialTokenFactory.connect(signers.alice).deployConfidentialToken( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.alice.address, // admin + signers.alice.address // wrapperSetter + ) + ).to.be.revertedWithCustomError(confidentialTokenFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.alice.address); + }); + + it("should allow owner to call deployConfidentialToken", async function () { + const tx = await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + }); + + it("should emit ConfidentialTokenDeployed event when deploying", async function () { + const name = "Confidential USDC"; + const symbol = "cUSDC"; + const decimals = 6; + const underlying = ethers.ZeroAddress; + + await expect( + confidentialTokenFactory.deployConfidentialToken( + implementation, + name, + symbol, + decimals, + 1, + underlying, + await adminProvider.getAddress(), + signers.deployer.address, + signers.deployer.address + ) + ).to.emit(confidentialTokenFactory, "ConfidentialTokenDeployed") + .withArgs( + implementation, + (confidentialToken: string) => ethers.isAddress(confidentialToken), // Check it's a valid address + name, + symbol, + decimals, + underlying + ); + }); + + it("should reuse the same implementation for multiple deployments", async function () { + // Deploy first token - use staticCall to get return value + const token1Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + + // Deploy second token - use staticCall to get return value + const token2Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + + // Verify proxies are different + expect(token1Address).to.not.equal(token2Address); + + // Get implementation addresses using ERC1967 storage slot + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const impl1 = await ethers.provider.getStorage(token1Address, implementationSlot); + const impl2 = await ethers.provider.getStorage(token2Address, implementationSlot); + + // Convert to addresses and verify they match + const impl1Address = "0x" + impl1.slice(26); + const impl2Address = "0x" + impl2.slice(26); + + expect(impl1Address.toLowerCase()).to.equal(implementation.toLowerCase()); + expect(impl2Address.toLowerCase()).to.equal(implementation.toLowerCase()); + }); + + it("should deploy functional tokens with shared implementation", async function () { + // Deploy two tokens with the same implementation using staticCall + const token1Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + + const token2Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ); + + // Get contract instances + const RegulatedERC7984 = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const token1 = RegulatedERC7984.attach(token1Address) as RegulatedERC7984Upgradeable; + const token2 = RegulatedERC7984.attach(token2Address) as RegulatedERC7984Upgradeable; + + // Verify tokens have correct metadata + expect(await token1.name()).to.equal("Confidential USDC"); + expect(await token1.symbol()).to.equal("cUSDC"); + expect(await token1.decimals()).to.equal(6); + + expect(await token2.name()).to.equal("Confidential DAI"); + expect(await token2.symbol()).to.equal("cDAI"); + expect(await token2.decimals()).to.equal(18); + + // Verify underlying addresses + expect(await token1.underlying()).to.equal(ethers.ZeroAddress); + expect(await token2.underlying()).to.equal(ethers.ZeroAddress); + }); + }); + + describe("Individual Token Upgrades (UUPS)", function () { + it("should allow upgrading individual tokens independently", async function () { + // Deploy two tokens with same implementation + const adminProviderOwner = await adminProvider.owner(); + + const token1Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + adminProviderOwner, // admin goes to adminProvider owner + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential USDC", + "cUSDC", + 6, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + adminProviderOwner, // admin goes to adminProvider owner + signers.deployer.address // wrapperSetter + ); + + const token2Address = await confidentialTokenFactory.deployConfidentialToken.staticCall( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + adminProviderOwner, // admin goes to adminProvider owner + signers.deployer.address // wrapperSetter + ); + await confidentialTokenFactory.deployConfidentialToken( + implementation, + "Confidential DAI", + "cDAI", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + adminProviderOwner, // admin goes to adminProvider owner + signers.deployer.address // wrapperSetter + ); + + // Get token contracts + const RegulatedERC7984 = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const token1 = RegulatedERC7984.attach(token1Address) as RegulatedERC7984Upgradeable; + const token2 = RegulatedERC7984.attach(token2Address) as RegulatedERC7984Upgradeable; + + // Verify both use original implementation + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const impl1Before = await ethers.provider.getStorage(token1Address, implementationSlot); + const impl1BeforeAddress = "0x" + impl1Before.slice(26); + const impl2Before = await ethers.provider.getStorage(token2Address, implementationSlot); + const impl2BeforeAddress = "0x" + impl2Before.slice(26); + + expect(impl1BeforeAddress.toLowerCase()).to.equal(implementation.toLowerCase()); + expect(impl2BeforeAddress.toLowerCase()).to.equal(implementation.toLowerCase()); + + // Deploy V2 implementation + const RegulatedERC7984V2 = await ethers.getContractFactory("RegulatedERC7984UpgradeableV2Mock"); + const v2Impl = await RegulatedERC7984V2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Grant UPGRADER_ROLE to alice for testing + await ethers.provider.send("hardhat_impersonateAccount", [adminProviderOwner]); + const adminSigner = await ethers.getSigner(adminProviderOwner); + const UPGRADER_ROLE = await token1.UPGRADER_ROLE(); + await token1.connect(adminSigner).grantRole(UPGRADER_ROLE, signers.alice.address); + await ethers.provider.send("hardhat_stopImpersonatingAccount", [adminProviderOwner]); + + // Upgrade ONLY token1 to V2 (as alice with UPGRADER_ROLE) + await token1.connect(signers.alice).upgradeToAndCall(v2ImplAddress, "0x"); + + // Verify token1 was upgraded + const impl1After = await ethers.provider.getStorage(token1Address, implementationSlot); + const impl1AfterAddress = "0x" + impl1After.slice(26); + expect(impl1AfterAddress.toLowerCase()).to.equal(v2ImplAddress.toLowerCase()); + + // Verify token2 still uses original implementation + const impl2After = await ethers.provider.getStorage(token2Address, implementationSlot); + const impl2AfterAddress = "0x" + impl2After.slice(26); + expect(impl2AfterAddress.toLowerCase()).to.equal(implementation.toLowerCase()); + + // Verify V2 functionality works on token1 + const token1V2 = RegulatedERC7984V2.attach(token1Address) as RegulatedERC7984UpgradeableV2Mock; + expect(await token1V2.counter()).to.equal(0); + await token1V2.incrementCounter(); + expect(await token1V2.counter()).to.equal(1); + + // Verify token2 doesn't have V2 functions (still V1) + const token2AsV2 = RegulatedERC7984V2.attach(token2Address) as RegulatedERC7984UpgradeableV2Mock; + await expect(token2AsV2.counter()).to.be.reverted; // V2 functions don't exist on V1 + + // Verify both tokens still have correct metadata + expect(await token1.name()).to.equal("Confidential USDC"); + expect(await token2.name()).to.equal("Confidential DAI"); + }); + }); + + describe("Ownership Management", function () { + it("should support two-step ownership transfer", async function () { + await confidentialTokenFactory.transferOwnership(signers.alice.address); + + expect(await confidentialTokenFactory.pendingOwner()).to.equal(signers.alice.address); + + expect(await confidentialTokenFactory.owner()).to.equal(signers.deployer.address); + + await confidentialTokenFactory.connect(signers.alice).acceptOwnership(); + + expect(await confidentialTokenFactory.owner()).to.equal(signers.alice.address); + expect(await confidentialTokenFactory.pendingOwner()).to.equal(ethers.ZeroAddress); + }); + + it("should prevent non-pending owner from accepting ownership", async function () { + await confidentialTokenFactory.transferOwnership(signers.alice.address); + + await expect( + confidentialTokenFactory.connect(signers.bob).acceptOwnership() + ).to.be.revertedWithCustomError(confidentialTokenFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.bob.address); + }); + + it("should allow new owner to deploy after ownership transfer", async function () { + await confidentialTokenFactory.transferOwnership(signers.alice.address); + await confidentialTokenFactory.connect(signers.alice).acceptOwnership(); + + const tx = await confidentialTokenFactory.connect(signers.alice).deployConfidentialToken( + implementation, + "Alice Token", + "ALICE", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.alice.address, // admin + signers.alice.address // wrapperSetter + ); + + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + + await expect( + confidentialTokenFactory.connect(signers.deployer).deployConfidentialToken( + implementation, + "Deployer Token", + "DEPLOY", + 18, + 1, + ethers.ZeroAddress, + await adminProvider.getAddress(), + signers.deployer.address, // admin + signers.deployer.address // wrapperSetter + ) + ).to.be.revertedWithCustomError(confidentialTokenFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.deployer.address); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Swap.ts b/protocol-contracts/confidential-wrapper/test/Swap.ts new file mode 100644 index 0000000000..b35aa76fb9 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Swap.ts @@ -0,0 +1,2355 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { AdminProvider, MaliciousWrapper, MaliciousWrapperAttacker, DeploymentCoordinator, FeeOnTransferERC20, RegulatedERC7984Upgradeable, SwapV0, TestERC20, UniswapV2Factory, UniswapV2Pair, UniswapV2Router02, WETH9, Wrapper } from "../types"; +import { deployConfidentialErc20Fixture, deploySwapV0Fixture, deployTestERC20Fixture, deployUniswapFactoryFixture, deployWrapperFixture } from "./fixtures"; +import { getSigners, Signers } from "./signers"; +import { ethers, fhevm } from "hardhat"; +import { deployConfidentialToken, getConfidentialBalance, getFeeManager, getSwapEvent, getSwapStartedEvent, getTxHashes, getUnwrapFee, getUnwrapFinalizedEvent, wrapERC20, finalizeUnwrapFromReceipt } from "./utils"; +import { expect } from "chai"; + + +async function addLiquidity( + deployer: HardhatEthersSigner, + router: UniswapV2Router02, + tokenA: TestERC20, + amountA: number, + tokenB: TestERC20, + amountB: number, +) { + await tokenA.connect(deployer).approve(router.target, 100_000_000); + await tokenB.connect(deployer).approve(router.target, 100_000_000); + await router.connect(deployer).addLiquidity( + tokenA.target, + tokenB.target, + amountA, + amountB, + 0, + 0, + deployer, + Math.floor(Date.now() / 1000) + 6000 + ); + + const factoryAddress = await router.factory(); + const factory = await ethers.getContractAt("UniswapV2Factory", factoryAddress); + const pairAddress = await factory.getPair(tokenA.target, tokenB.target); + return await ethers.getContractAt("UniswapV2Pair", pairAddress); +} + + +async function getSwapFinalizedReceipt(transferReceipt: any, wrapper: Wrapper, signer: HardhatEthersSigner) { + return await finalizeUnwrapFromReceipt(transferReceipt, wrapper, signer); +} + + +describe("Uniswap V2 Swap Test", function () { + let signers: Signers; + let tokenA: TestERC20; + let coordinator: DeploymentCoordinator; + let cTokenA: RegulatedERC7984Upgradeable; + let wrapperA: Wrapper; + let tokenB: TestERC20; + let cTokenB: RegulatedERC7984Upgradeable; + let wrapperB: Wrapper; + let router: UniswapV2Router02; + let factory: UniswapV2Factory; + let weth: WETH9; + let pair: UniswapV2Pair; + let swapV0: SwapV0; + let adminProvider: AdminProvider; + + beforeEach(async function () { + signers = await getSigners(); + ({ router, factory, weth } = await deployUniswapFactoryFixture(signers.admin)); + + tokenA = await deployTestERC20Fixture("TOK_A", 6); + tokenB = await deployTestERC20Fixture("TOK_B", 6); + await tokenA.mint(signers.alice, 100_000_000_000); + await tokenB.mint(signers.alice, 100_000_000_000); + + pair = await addLiquidity(signers.alice, router, tokenA, 100_000_000, tokenB, 100_000_000); + + ({ coordinator, adminProvider } = await deployWrapperFixture(signers)); + ({ cToken: cTokenA, wrapper: wrapperA } = await deployConfidentialToken(coordinator, tokenA, signers.alice)); + ({ cToken: cTokenB, wrapper: wrapperB } = await deployConfidentialToken(coordinator, tokenB, signers.alice)); + + await wrapERC20(coordinator, tokenA, BigInt(100_000), signers.alice.address, signers.alice); + await wrapERC20(coordinator, tokenB, BigInt(100_000), signers.alice.address, signers.alice); + + ({ swapV0 } = await deploySwapV0Fixture(coordinator)); + + // Whitelist the router for all tests (except the specific test that checks non-whitelisted behavior) + await swapV0.addRouterToWhitelist(await router.getAddress()); + + // Whitelist the tokens for all tests (except the specific tests that check non-whitelisted behavior) + await swapV0.addTokenToWhitelist(await tokenA.getAddress()); + await swapV0.addTokenToWhitelist(await tokenB.getAddress()); + }); + + it("should swap cTokenA for cTokenB", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const feeManager = await getFeeManager(coordinator); + const unwrapFee = await getUnwrapFee(wrapperA, transferAmount); + + const swapInAmount = transferAmount - unwrapFee; + const swapOutAmounts = await router.getAmountsOut(swapInAmount, path); + const swapOutAmount = swapOutAmounts[swapOutAmounts.length - 1]; + + const wrapFee = await feeManager.getWrapFee(swapOutAmount, ethers.ZeroAddress, ethers.ZeroAddress); + const amountCTokenBOut = swapOutAmount - wrapFee; + + // Get the next transaction IDs before the swap for event verification + const expectedWrapTxId = await cTokenB.nextTxId(); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ) + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ) + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ) + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizedReceipt); + expect(unwrapFinalizedEvents).to.have.length(1); + + // Verify Swap event was emitted with correct parameters + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + const swapEvent = swapEvents[0]; + expect(swapEvent.args.success).to.equal(true); + expect(swapEvent.args.path).to.deep.equal(path); + expect(swapEvent.args.unwrapTxId).to.equal(unwrapFinalizedEvents[0].args.requestId); + expect(swapEvent.args.wrapTxId).to.equal(expectedWrapTxId); + expect(swapEvent.args.errorReasonString).to.equal(""); + expect(swapEvent.args.errorLowLevelData).to.equal("0x"); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + expect(balanceCTokenBAfter - balanceCTokenBBefore).to.equal(amountCTokenBOut); + }); + + it("should return cTokenA when swapping fails", async function () { + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const feeManager = await getFeeManager(coordinator); + const unwrapFee = await getUnwrapFee(wrapperA, transferAmount); + + const swapInAmount = transferAmount - unwrapFee; + + const wrapFee = await feeManager.getWrapFee(swapInAmount, ethers.ZeroAddress, ethers.ZeroAddress); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) - 1, + signers.alice.address, + ]] + ) + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ) + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ) + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap (not Alice) - this tests that refunds go to the finalizer + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizedReceipt); + expect(unwrapFinalizedEvents).to.have.length(1); + + // Verify Swap event was emitted with failure parameters + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + const swapEvent = swapEvents[0]; + expect(swapEvent.args.success).to.equal(false); + expect(swapEvent.args.path).to.deep.equal(path); + expect(swapEvent.args.unwrapTxId).to.equal(unwrapFinalizedEvents[0].args.requestId); + expect(swapEvent.args.wrapTxId).to.equal(5); // wrap transaction for reimbursement + expect(swapEvent.args.errorReasonString).to.equal("UniswapV2Router: EXPIRED"); + expect(swapEvent.args.errorLowLevelData).to.equal("0x"); + + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Alice's balance decreases by the transfer amount (she initiated the swap) + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund minus fees (he finalized the unwrap, so refund goes to him) + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount - unwrapFee - wrapFee); + + // No cTokenB received (swap failed) + expect(balanceCTokenBAfter - balanceCTokenBBefore).to.equal(0); + }); + + it("should swap TokenA for TokenB", async function () { + const amountIn = 10_000_000; + + await tokenA.connect(signers.alice).approve(router.target, amountIn); + + const path = [tokenA.target, tokenB.target]; + const deadline = Math.floor(Date.now() / 1000) + 6000; + + const beforeBalance = await tokenB.balanceOf(signers.alice); + + await router.connect(signers.alice).swapExactTokensForTokens( + amountIn, + 0, + path, + signers.alice, + deadline + ); + + const afterBalance = await tokenB.balanceOf(signers.alice); + }); + + it("should not charge wrap/unwrap fees for SWAPPER role when waiver is active", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + + // Grant SWAPPER role to the SwapV0 contract + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + + // Activate swapper fee waiver + await feeManager.setSwapperFeeWaiverActive(true); + + // Set non-zero fees to verify they are waived + await feeManager.setWrapFeeBasisPoints(100); // 1% + await feeManager.setUnwrapFeeBasisPoints(100); // 1% + + const transferAmount = BigInt(10_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + // Calculate expected amounts without fees since SwapV0 has SWAPPER role + // Since SwapV0 has SWAPPER role, full transfer amount goes to swap (no unwrap fee) + const swapInAmountWithFeeWaiver = transferAmount; + const swapOutAmountsWithFeeWaiver = await router.getAmountsOut(swapInAmountWithFeeWaiver, path); + const swapOutAmountWithFeeWaiver = swapOutAmountsWithFeeWaiver[swapOutAmountsWithFeeWaiver.length - 1]; + const expectedAmountCTokenBOut = swapOutAmountWithFeeWaiver; // No wrap fee either + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + await finalizeUnwrapFromReceipt(transferReceipt, wrapperA, signers.alice); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Verify full transfer amount was used (no unwrap fee) + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + + // Verify full transfer amount was used (no unwrap fee) and full swap output was received (no wrap fee) + expect(balanceCTokenBAfter - balanceCTokenBBefore).to.equal(expectedAmountCTokenBOut); + + // Verify that the FeeManager correctly returns 0 fees for the SwapV0 contract + const wrapFee = await feeManager.getWrapFee(10000, await swapV0.getAddress(), ethers.ZeroAddress); + expect(wrapFee).to.equal(0); + + // Verify that SwapV0 has the SWAPPER role and waiver is active + expect(await feeManager.hasRole(swapperRole, await swapV0.getAddress())).to.be.true; + expect(await feeManager.swapperFeeWaiverActive()).to.be.true; + }); + + describe("forceApprove with USDT-like tokens", function () { + it("should handle forceApprove for router approval (line 75) with USDT-like token", async function () { + // Deploy USDT-like tokenA that requires 0-approval before changing + const USDTMockFactory = await ethers.getContractFactory("ERC20USDTApprovalMock"); + const usdtLikeTokenA = await USDTMockFactory.deploy("USDT-like TokenA", "USDTA", 6); + await usdtLikeTokenA.waitForDeployment(); + await usdtLikeTokenA.mint(signers.alice, 100_000_000_000); + + // Add liquidity with USDT-like token + await addLiquidity(signers.alice, router, usdtLikeTokenA, 100_000_000, tokenB, 100_000_000); + + // Deploy wrapper for USDT-like token + const { cToken: cUsdtLikeTokenA, wrapper: cUsdtLikeWrapperA } = await deployConfidentialToken(coordinator, usdtLikeTokenA, signers.alice); + await wrapERC20(coordinator, usdtLikeTokenA, BigInt(100_000), signers.alice.address, signers.alice); + + // Use test helper to set non-zero approval from SwapV0 to router + // This simulates a scenario where SwapV0 already has approval set + await usdtLikeTokenA.testSetApproval(await swapV0.getAddress(), await router.getAddress(), 1000); + + // Verify the approval is set + const approvalBefore = await usdtLikeTokenA.allowance(await swapV0.getAddress(), await router.getAddress()); + expect(approvalBefore).to.equal(1000); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cUsdtLikeTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await usdtLikeTokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cUsdtLikeTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await cUsdtLikeWrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Should succeed despite existing approval (forceApprove handles it) + await finalizeUnwrapFromReceipt(transferReceipt, cUsdtLikeWrapperA, signers.alice); + }); + + it("should handle forceApprove for wrapper approval after successful swap (line 105) with USDT-like tokenOut", async function () { + // Deploy USDT-like tokenB as output + const USDTMockFactory = await ethers.getContractFactory("ERC20USDTApprovalMock"); + const usdtLikeTokenB = await USDTMockFactory.deploy("USDT-like TokenB", "USDTB", 6); + await usdtLikeTokenB.waitForDeployment(); + await usdtLikeTokenB.mint(signers.alice, 100_000_000_000); + + // Add liquidity + await addLiquidity(signers.alice, router, tokenA, 100_000_000, usdtLikeTokenB, 100_000_000); + + // Deploy wrapper for USDT-like tokenB + const { wrapper: wrapperUsdtB } = await deployConfidentialToken(coordinator, usdtLikeTokenB, signers.alice); + + // Use test helper to set non-zero approval from SwapV0 to wrapperUsdtB + // This simulates a scenario where SwapV0 already has approval set + await usdtLikeTokenB.testSetApproval(await swapV0.getAddress(), await wrapperUsdtB.getAddress(), 1000); + + // Verify the approval is set + const approvalBefore = await usdtLikeTokenB.allowance(await swapV0.getAddress(), await wrapperUsdtB.getAddress()); + expect(approvalBefore).to.equal(1000); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await usdtLikeTokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Should succeed - forceApprove handles the non-zero approval by resetting to 0 first + // Without forceApprove, this would revert with ApprovalFromNonZeroToNonZero error + await finalizeUnwrapFromReceipt(transferReceipt, wrapperA, signers.alice); + }); + + it("should handle forceApprove for wrapper approval after failed swap (line 116) with USDT-like tokenIn", async function () { + // Deploy USDT-like tokenA + const USDTMockFactory = await ethers.getContractFactory("ERC20USDTApprovalMock"); + const usdtLikeTokenA = await USDTMockFactory.deploy("USDT-like TokenA", "USDTA", 6); + await usdtLikeTokenA.waitForDeployment(); + await usdtLikeTokenA.mint(signers.alice, 100_000_000_000); + + // Add liquidity + await addLiquidity(signers.alice, router, usdtLikeTokenA, 100_000_000, tokenB, 100_000_000); + + // Deploy wrapper for USDT-like token + const { cToken: cUsdtLikeTokenA, wrapper: wrapperUsdtA } = await deployConfidentialToken(coordinator, usdtLikeTokenA, signers.alice); + await wrapERC20(coordinator, usdtLikeTokenA, BigInt(100_000), signers.alice.address, signers.alice); + + // Use test helper to set non-zero approval from SwapV0 to wrapperUsdtA + // This simulates a scenario where SwapV0 already has approval set + await usdtLikeTokenA.testSetApproval(await swapV0.getAddress(), await wrapperUsdtA.getAddress(), 1000); + + // Verify the approval is set + const approvalBefore = await usdtLikeTokenA.allowance(await swapV0.getAddress(), await wrapperUsdtA.getAddress()); + expect(approvalBefore).to.equal(1000); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cUsdtLikeTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await usdtLikeTokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) - 1, // Expired deadline to force swap failure + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cUsdtLikeTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperUsdtA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Should succeed - forceApprove handles the non-zero approval by resetting to 0 first + // Without forceApprove, this would revert with ApprovalFromNonZeroToNonZero error + await finalizeUnwrapFromReceipt(transferReceipt, wrapperUsdtA, signers.alice); + }); + }); + + describe("Path Validation", function () { + it("should reject swap when cToken.underlying() does not match path[0]", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Create path that starts with tokenB instead of tokenA (cTokenA.underlying()) + const invalidPath = [await tokenB.getAddress(), await tokenA.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + invalidPath, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.INPUT_PATH_IS_NOT_UNDERLYING()); + + // Verify balances: Alice loses the transfer, Bob gets refunded + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Alice's balance decreased (she initiated the swap) + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund (he finalized, so refund goes to him) + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount); + + expect(balanceCTokenBAfter).to.equal(balanceCTokenBBefore); + }); + + it("should reject swap when no coordinator wrapper exists for input token (path[0])", async function () { + // create an alternate coordinator that is unknown to the swapper + // we'll deploy a tokenC on this alternat coordinator so that all interfaces match + // and we can call the swapper. Any swap from cTokenC with a path starting at tokenC + // should thus fail. + const { coordinator: coordinatorAlt } = await deployWrapperFixture(signers); + + const feeManager = await getFeeManager(coordinatorAlt); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + + // deploy tokenC wrapper in different coordinator than that of swap + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + const {cToken: cTokenC, wrapper: wrapperC} = await deployConfidentialToken(coordinatorAlt, tokenC, signers.alice); + await wrapERC20(coordinatorAlt, tokenC, BigInt(100_000_000), signers.alice.address, signers.alice); + + // Whitelist tokenC so we can test the wrapper validation (not token whitelist validation) + await swapV0.addTokenToWhitelist(await tokenC.getAddress()); + + // Add liquidity for tokenC -> tokenB swap + await addLiquidity(signers.alice, router, tokenC, 100_000_000, tokenB, 100_000_000); + + const balanceAliceCTokenCBefore = await getConfidentialBalance(cTokenC, signers.alice); + const balanceBobCTokenCBefore = 0n; + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenC.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Create path with tokenC (no wrapper at coordinator level) as input + const invalidPath = [await tokenC.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + invalidPath, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + // Try to swap cTokenC with path that starts with tokenC + // This should fail because cTokenC does not have a wrapper with + // the configured coordinator + const transferTx = await cTokenC.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperC.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + await expect(getSwapFinalizedReceipt(transferReceipt, wrapperC, signers.alice)).to.be.revertedWithCustomError(swapV0, "UnknownWrapper"); + }); + + it("should reject swap when no wrapper exists for output token (path[path.length-1])", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + // Deploy a new token without creating a wrapper for it + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + + // deploy a confidential token without a wrapper. Athough path[-1] matches the cToken's + // underlying, the lack of wrapper will make the swap fail and refund. + const { cErc20: cTokenC } = await deployConfidentialErc20Fixture(signers, adminProvider, tokenC); + await cTokenC.mint(signers.alice, 100_000_000_000); + + // Whitelist tokenC so we can test the wrapper validation (not token whitelist validation) + await swapV0.addTokenToWhitelist(await tokenC.getAddress()); + + // Add liquidity for tokenA -> tokenC swap + await addLiquidity(signers.alice, router, tokenA, 100_000_000, tokenC, 100_000_000); + + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Create path with tokenC (no wrapper) as output + const invalidPath = [await tokenA.getAddress(), await tokenC.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + invalidPath, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.OUTPUT_PATH_HAS_NO_WRAPPER()); + + // Verify balances: Alice loses the transfer, Bob gets refunded + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + + // Alice's balance decreased + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount); + }); + + for (let createPool of [true, false]) { + it("should reject swap when path does not exist at Uniswap router level", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + // Deploy a new token and create wrapper but don't add liquidity + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + + // Create wrapper for tokenC so it passes the wrapper checks + const { cToken: cTokenC } = await deployConfidentialToken(coordinator, tokenC, signers.alice); + + // Whitelist tokenC so we can test the Uniswap path validation (not token whitelist validation) + await swapV0.addTokenToWhitelist(await tokenC.getAddress()); + + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + await router.factory() + + if (createPool) { + const factory = await ethers.getContractAt("UniswapV2Factory", await router.factory()) as UniswapV2Factory; + await factory.createPair(await tokenA.getAddress(), await tokenC.getAddress()); + } + + // Create path tokenA -> tokenC (no liquidity, so getAmountsOut will fail) + const invalidPath = [await tokenA.getAddress(), await tokenC.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + invalidPath, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const receipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const swapFinalizedReceipt = await getSwapFinalizedReceipt(receipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + if (createPool) { + expect(swapEvents[0].args.errorReasonString).to.equal("UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + } else { + expect(swapEvents[0].args.errorReasonString).to.equal(""); + } + expect(swapEvents[0].args.errorLowLevelData).to.equal("0x"); + + // Verify balances: Alice loses the transfer, Bob gets refunded + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + + // Alice's balance decreased + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount); + }); + } + + it("should expose checkPath as a public view function", async function () { + // Simple test to verify checkPath is accessible as a public view + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + // Call checkPath - should not revert and return expected structure + const [isValid, errorString, errorData] = await swapV0.checkPath( + wrapperA, + await router.getAddress(), + path + ); + + expect(isValid).to.equal(true); + expect(errorString).to.equal(""); + expect(errorData).to.equal("0x"); + }); + + it("should handle valid path that fails getAmountsOut(1) due to rounding", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + // Deploy tokenC and create a pool with very low liquidity + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + + // Create wrapper for tokenC + const { cToken: cTokenC, wrapper: wrapperC } = await deployConfidentialToken(coordinator, tokenC, signers.alice); + + // Whitelist tokenC so we can test the rounding behavior (not token whitelist validation) + await swapV0.addTokenToWhitelist(await tokenC.getAddress()); + + // Wrap some cTokenA for Alice to swap + await wrapERC20(coordinator, tokenA, BigInt(100_000), signers.alice.address, signers.alice); + + // Add liquidity with very imbalanced reserves to cause rounding issues + // Pool: 1 tokenA : 1,000,000 tokenB (tokenA is very valuable) + await addLiquidity(signers.alice, router, tokenA, 1, tokenB, 1_000_000); + + // Add liquidity for second hop: 1,000,000 tokenB : 1,000,000 tokenC + await addLiquidity(signers.alice, router, tokenB, 1_000_000, tokenC, 1_000_000); + + // Create a valid path: tokenA -> tokenB -> tokenC + const path = [ + await tokenA.getAddress(), + await tokenB.getAddress(), + await tokenC.getAddress() + ]; + + // Verify that getAmountsOut(1) FAILS due to rounding + await expect( + router.getAmountsOut(1, path) + ).to.be.reverted; + + // But verify that larger amounts work fine (proving the path is actually valid) + const amounts = await router.getAmountsOut(1000, path); + expect(amounts.length).to.equal(3); + expect(amounts[0]).to.equal(1000); + expect(amounts[2]).to.be.greaterThan(0); + + const [isValid, errorString, errorData] = await swapV0.checkPath( + wrapperA, + await router.getAddress(), + path + ); + + // checkPath returns true although getAmountsOut fails + expect(isValid).to.equal(true); + + // Now attempt an actual swap to ensure proper refund + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenCBefore = 0n; + + const transferAmount = BigInt(1); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + // Verify Swap event still failed + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + + expect(swapEvents[0].args.success).to.equal(false); + + // Verify Alice gets refunded (swap failed) + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenCAfter = 0n; + + // Alice should have received her tokens back + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(0); + expect(balanceCTokenCAfter).to.equal(balanceCTokenCBefore); // No tokenC received + }); + }); + + describe("Security: Zero Address Recipient Protection", function () { + it("should reject swap with address(0) recipient and refund to refundTo address", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + ethers.ZeroAddress, // address(0) as recipient - this is the vulnerability + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, // Bob is the refundTo address + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.RECIPIENT_CANNOT_BE_ZERO_ADDRESS()); + + // Verify balances: Alice loses the transfer, Bob gets refunded + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + + // Alice's balance decreased (she initiated the swap) + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund (he is the refundTo address) + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount); + + // Verify no funds stuck in SwapV0 + expect(await tokenA.balanceOf(await swapV0.getAddress())).to.equal(0); + }); + }); + + describe("Security: Router Whitelist in Swaps", function () { + it("should reject swap when router is not whitelisted", async function () { + const feeManager = await getFeeManager(coordinator); + const swapperRole = await feeManager.SWAPPER_ROLE(); + await feeManager.grantRole(swapperRole, await swapV0.getAddress()); + await feeManager.setSwapperFeeWaiverActive(true); + + // Use a fake/malicious router address (not whitelisted) + const fakeRouter = await ethers.Wallet.createRandom().getAddress(); + + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + fakeRouter, // Use non-whitelisted router + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - should receive refund because router not whitelisted + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.ROUTER_NOT_WHITELISTED()); + + // Verify balances: Alice loses the transfer, Bob gets refunded + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + + // Alice's balance decreased (she initiated the swap) + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund (he finalized, so refund goes to him) + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(transferAmount); + }); + }); + + describe("Security: Unauthorized Wrapper Attack", function () { + it("should block malicious wrapper from calling onUnwrapFinalizedReceived directly", async function () { + // Deploy the MaliciousWrapper contract that attempts to bypass fees + const MaliciousWrapperFactory = await ethers.getContractFactory("MaliciousWrapper"); + const maliciousWrapper = await MaliciousWrapperFactory.deploy( + await tokenA.getAddress(), + await swapV0.getAddress() + ); + await maliciousWrapper.waitForDeployment(); + + // Set fees to demonstrate what the attack would have bypassed + const feeManager = await getFeeManager(coordinator); + await feeManager.setWrapFeeBasisPoints(100); // 1% wrap fee + await feeManager.setUnwrapFeeBasisPoints(100); // 1% unwrap fee + + const swapAmount = BigInt(10_000); + + // Approve malicious wrapper to spend alice's tokens + await tokenA.connect(signers.alice).approve(await maliciousWrapper.getAddress(), swapAmount); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + const deadline = Math.floor(Date.now() / 1000) + 6000; + + // Attempt the attack - should revert because SwapV0 validates msg.sender + // The malicious wrapper tries to call onUnwrapFinalizedReceived directly to bypass unwrap fees + // SwapV0's checkPath function verifies that msg.sender matches the legitimate wrapper + // registered in the coordinator for the underlying token + await expect( + maliciousWrapper.connect(signers.alice).attemptExploit( + swapAmount, + await router.getAddress(), + 0, // amountOutMin + path, + deadline, + signers.alice.address + ) + ).to.be.revertedWithCustomError(swapV0, "UnknownWrapper"); + + // Verify no tokens were transferred (attack was blocked before any state changes) + expect(await tokenA.balanceOf(await maliciousWrapper.getAddress())).to.equal(0); + expect(await tokenA.balanceOf(await swapV0.getAddress())).to.equal(0); + }); + }); + + describe("Fee-on-Transfer (FOT) Token Support", function () { + it("should handle FOT swap fails and refunds user", async function () { + // The issue was that SwapV0 would attempt to wrap amountIn tokens during refund, + // but only held actualBalance < amountIn due to fee-on-transfer, causing revert and stuck funds. + // The fix checks actual balance and only wraps what the contract actually holds. + + // Deploy FOT token with 2% transfer fee (200 basis points) + const FeeOnTransferERC20Factory = await ethers.getContractFactory("FeeOnTransferERC20"); + const feeToken = await FeeOnTransferERC20Factory.deploy("FeeToken", "FEE", 6, 200); + await feeToken.waitForDeployment(); + await feeToken.mint(signers.alice, 100_000_000_000); + + // Deploy confidential FOT token and wrapper + const { cToken: cFOT, wrapper: wrapperFOT } = await deployConfidentialToken(coordinator, feeToken, signers.alice); + + // Whitelist feeToken so we can test the FOT behavior (not token whitelist validation) + await swapV0.addTokenToWhitelist(await feeToken.getAddress()); + + // Wrap FOT tokens + await wrapERC20(coordinator, feeToken, BigInt(100_000), signers.alice.address, signers.alice); + + // Add liquidity with FOT token + await addLiquidity(signers.alice, router, feeToken, 100_000_000, tokenB, 100_000_000); + + const balanceAliceCFOTBefore = await getConfidentialBalance(cFOT, signers.alice); + const balanceBobCFOTBefore = 0n; + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cFOT.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await feeToken.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) - 1, // Expired deadline to force swap failure + signers.alice.address, + ]] + ); + const data = abiCoder.encode(["address", "address", "bytes"], [await swapV0.getAddress(), signers.bob.address, callbackData]); + + const transferTx = await cFOT.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperFOT.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data + ); + const transferReceipt = await transferTx.wait(); + + // KEY TEST: Bob finalizes the unwrap - refund should go to Bob (not Alice) + // This should succeed now (with the fix) - SwapV0 checks actual balance and only wraps what it has + // Without the fix, this would revert with ERC20InsufficientBalance + const finalizedReceipt = await finalizeUnwrapFromReceipt(transferReceipt, wrapperFOT, signers.alice); + + // Verify Swap event shows failure + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal("UniswapV2Router: EXPIRED"); + + const balanceAliceCFOTAfter = await getConfidentialBalance(cFOT, signers.alice); + const balanceBobCFOTAfter = await getConfidentialBalance(cFOT, signers.bob); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Alice's balance decreased (she initiated the swap) + expect(balanceAliceCFOTBefore - balanceAliceCFOTAfter).to.equal(transferAmount); + + // Bob receives the refund (he finalized the unwrap) + // Due to FOT fees, Bob gets less than the original transfer amount + const bobRefund = balanceBobCFOTAfter - balanceBobCFOTBefore; + expect(bobRefund).to.be.lessThan(transferAmount); // Bob gets less due to FOT fees + expect(bobRefund).to.be.greaterThan(0); // Bob got a partial refund + + // Verify no cTokenB was received (swap failed) + expect(balanceCTokenBAfter).to.equal(balanceCTokenBBefore); + + // Verify SwapV0 has no leftover tokens (all refunded to user) + expect(await feeToken.balanceOf(await swapV0.getAddress())).to.equal(0); + }); + }); + + describe("SwapV0 Router Whitelist Management", function () { + it("should allow owner to add router to whitelist", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + + // Verify router not whitelisted initially + expect(await swapV0.whitelistedRouters(testRouter)).to.equal(false); + + // Add router to whitelist + await expect(swapV0.addRouterToWhitelist(testRouter)) + .to.emit(swapV0, "RouterAddedToWhitelist") + .withArgs(testRouter); + + // Verify router is now whitelisted + expect(await swapV0.whitelistedRouters(testRouter)).to.equal(true); + }); + + it("should allow owner to remove router from whitelist", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + + // Add router first + await swapV0.addRouterToWhitelist(testRouter); + expect(await swapV0.whitelistedRouters(testRouter)).to.equal(true); + + // Remove router from whitelist + await expect(swapV0.removeRouterFromWhitelist(testRouter)) + .to.emit(swapV0, "RouterRemovedFromWhitelist") + .withArgs(testRouter); + + // Verify router is no longer whitelisted + expect(await swapV0.whitelistedRouters(testRouter)).to.equal(false); + }); + + it("should revert when adding zero address to whitelist", async function () { + await expect( + swapV0.addRouterToWhitelist(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(swapV0, "ZeroAddressRouter"); + }); + + it("should revert when adding already whitelisted router", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + await swapV0.addRouterToWhitelist(testRouter); + + await expect( + swapV0.addRouterToWhitelist(testRouter) + ).to.be.revertedWithCustomError(swapV0, "RouterAlreadyWhitelisted"); + }); + + it("should revert when removing zero address from whitelist", async function () { + await expect( + swapV0.removeRouterFromWhitelist(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(swapV0, "ZeroAddressRouter"); + }); + + it("should revert when removing router that is not whitelisted", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + + await expect( + swapV0.removeRouterFromWhitelist(testRouter) + ).to.be.revertedWithCustomError(swapV0, "RouterNotWhitelisted"); + }); + + it("should revert when non-owner tries to add router", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + + await expect( + swapV0.connect(signers.alice).addRouterToWhitelist(testRouter) + ).to.be.revertedWithCustomError(swapV0, "OwnableUnauthorizedAccount"); + }); + + it("should revert when non-owner tries to remove router", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + await swapV0.addRouterToWhitelist(testRouter); + + await expect( + swapV0.connect(signers.alice).removeRouterFromWhitelist(testRouter) + ).to.be.revertedWithCustomError(swapV0, "OwnableUnauthorizedAccount"); + }); + + it("should allow adding multiple different routers", async function () { + const router1 = await ethers.Wallet.createRandom().getAddress(); + const router2 = await ethers.Wallet.createRandom().getAddress(); + const router3 = await ethers.Wallet.createRandom().getAddress(); + + await swapV0.addRouterToWhitelist(router1); + await swapV0.addRouterToWhitelist(router2); + await swapV0.addRouterToWhitelist(router3); + + expect(await swapV0.whitelistedRouters(router1)).to.equal(true); + expect(await swapV0.whitelistedRouters(router2)).to.equal(true); + expect(await swapV0.whitelistedRouters(router3)).to.equal(true); + }); + + it("should allow re-adding a router after removal", async function () { + const testRouter = await ethers.Wallet.createRandom().getAddress(); + + // Add, remove, then re-add + await swapV0.addRouterToWhitelist(testRouter); + await swapV0.removeRouterFromWhitelist(testRouter); + await swapV0.addRouterToWhitelist(testRouter); + + expect(await swapV0.whitelistedRouters(testRouter)).to.equal(true); + }); + + it("should support two-step ownership transfer", async function () { + // SwapV0 is deployed by the default signer (first from ethers.getSigners()) + const deployer = (await ethers.getSigners())[0]; + + await swapV0.connect(deployer).transferOwnership(signers.alice.address); + expect(await swapV0.pendingOwner()).to.equal(signers.alice.address); + expect(await swapV0.owner()).to.equal(deployer.address); + + await swapV0.connect(signers.alice).acceptOwnership(); + expect(await swapV0.owner()).to.equal(signers.alice.address); + expect(await swapV0.pendingOwner()).to.equal(ethers.ZeroAddress); + }); + }); + + describe("Sanctioned Address Handling", function () { + it("should gracefully handle swap when recipient (to) is sanctioned - fallback to refundTo", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.bob.address, // Bob is the recipient + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, // Alice is refundTo + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Get sanctions list and sanction Bob BEFORE finalization + const adminProviderAddress = await coordinator.adminProvider(); + const adminProviderContract = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const sanctionsListAddress = await adminProviderContract.sanctionsList(); + const sanctionsList = await ethers.getContractAt("SanctionsList", sanctionsListAddress); + + await sanctionsList.addToSanctionsList([signers.bob.address]); + + // Finalize the unwrap - should succeed because of fallback to refundTo (alice) + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + // Verify Swap event shows success + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.RECIPIENT_IS_SANCTIONED()); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Alice's cTokenA balance decreased + const feeManager = await getFeeManager(coordinator); + const wrapFee = await feeManager.getWrapFee(transferAmount, ethers.ZeroAddress, ethers.ZeroAddress); + const unwrapFee = await getUnwrapFee(wrapperA, transferAmount - wrapFee); + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(wrapFee + unwrapFee); + + // Alice received no cTokenB + expect(balanceCTokenBBefore - balanceCTokenBAfter).to.equal(0); + + // Bob should have 0 cTokenB (sanctioned, so fallback kicked in) + const balanceBobCTokenBAfterHandle = await cTokenB.confidentialBalanceOf(signers.bob); + expect(Number(balanceBobCTokenBAfterHandle)).to.equal(0); + }); + + it("should gracefully handle swap when both recipient (to) and refundTo are sanctioned - fallback to feeRecipient", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.bob.address, // Bob is the recipient + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.charlie.address, // Charlie is refundTo + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Get sanctions list and sanction BOTH Bob and Charlie BEFORE finalization + const adminProviderAddress = await coordinator.adminProvider(); + const adminProviderContract = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const sanctionsListAddress = await adminProviderContract.sanctionsList(); + const sanctionsList = await ethers.getContractAt("SanctionsList", sanctionsListAddress); + + await sanctionsList.addToSanctionsList([signers.bob.address, signers.charlie.address]); + + // Get fee recipient before swap + const feeManager = await getFeeManager(coordinator); + const feeRecipient = await feeManager.feeRecipient(); + const balanceFeeRecipientCTokenBBefore = 0n; + + // Finalize the unwrap - should succeed because of final fallback to feeRecipient + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + // Verify Swap event shows success + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.REFUND_ADDRESS_IS_SANCTIONED()); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + + // Alice's cTokenA balance decreased + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + + // Fee recipient receives the cTokenA (final fallback) + const balanceFeeRecipientCTokenAAfter = await getConfidentialBalance(cTokenA, feeRecipient, signers.royalties); + const wrapFee = await feeManager.getWrapFee(transferAmount, ethers.ZeroAddress, ethers.ZeroAddress); + const unwrapFee = await getUnwrapFee(wrapperA, transferAmount - wrapFee); + expect(balanceFeeRecipientCTokenAAfter).to.equal(transferAmount - wrapFee - unwrapFee); + + // Bob and Charlie should have 0 cTokenB (both sanctioned) + const balanceBobCTokenBAfterHandle = await cTokenB.confidentialBalanceOf(signers.bob); + expect(Number(balanceBobCTokenBAfterHandle)).to.equal(0); + const balanceCharlieCTokenBAfterHandle = await cTokenB.confidentialBalanceOf(signers.charlie); + expect(Number(balanceCharlieCTokenBAfterHandle)).to.equal(0); + }); + + it("should prevent fund loss when recipient gets sanctioned between phase 1 and phase 2", async function () { + // This test reproduces the vulnerability scenario from the bug report + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.bob.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, // Alice is refundTo + callbackData, + ] + ); + + // Phase 1: confidentialTransferAndCall (burns tokens, stores callback data) + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Sanction Bob AFTER phase 1 but BEFORE phase 2 + const adminProviderAddress = await coordinator.adminProvider(); + const adminProviderContract = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const sanctionsListAddress = await adminProviderContract.sanctionsList(); + const sanctionsList = await ethers.getContractAt("SanctionsList", sanctionsListAddress); + + await sanctionsList.addToSanctionsList([signers.bob.address]); + + // Phase 2: finalizeUnwrap (should NOT revert thanks to the fix) + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + // Verify swap succeeded with fallback to refundTo + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfterAlice = await getConfidentialBalance(cTokenB, signers.alice); + const balanceCTokenBAfterBob = await cTokenB.confidentialBalanceOf(signers.bob); + + // Alice's cTokenA decreased + const feeManager = await getFeeManager(coordinator); + const wrapFee = await feeManager.getWrapFee(transferAmount, ethers.ZeroAddress, ethers.ZeroAddress); + const unwrapFee = await getUnwrapFee(wrapperA, transferAmount - wrapFee); + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(wrapFee + unwrapFee); + + // Bob receives nothing (sanctioned) + expect(Number(balanceCTokenBAfterBob)).to.equal(0); + }); + }); + + describe("Token Whitelist", function () { + it("should allow owner to add token to whitelist", async function () { + // Create a new token that's not whitelisted in beforeEach + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + const tokenAddress = await tokenC.getAddress(); + + // Verify token is not whitelisted initially + expect(await swapV0.whitelistedTokens(tokenAddress)).to.be.false; + + // Add token to whitelist + const tx = await swapV0.addTokenToWhitelist(tokenAddress); + const receipt = await tx.wait(); + + // Verify event was emitted + const events = receipt!.logs + .map((log: any) => { + try { + return swapV0.interface.parseLog({ topics: log.topics, data: log.data }); + } catch { + return null; + } + }) + .filter((parsed) => parsed?.name === "TokenAddedToWhitelist"); + + expect(events).to.have.length(1); + expect(events[0]!.args.token).to.equal(tokenAddress); + + // Verify token is now whitelisted + expect(await swapV0.whitelistedTokens(tokenAddress)).to.be.true; + }); + + it("should allow owner to remove token from whitelist", async function () { + // Use tokenA which is already whitelisted in beforeEach + const tokenAddress = await tokenA.getAddress(); + expect(await swapV0.whitelistedTokens(tokenAddress)).to.be.true; + + // Remove token from whitelist + const tx = await swapV0.removeTokenFromWhitelist(tokenAddress); + const receipt = await tx.wait(); + + // Verify event was emitted + const events = receipt!.logs + .map((log: any) => { + try { + return swapV0.interface.parseLog({ topics: log.topics, data: log.data }); + } catch { + return null; + } + }) + .filter((parsed) => parsed?.name === "TokenRemovedFromWhitelist"); + + expect(events).to.have.length(1); + expect(events[0]!.args.token).to.equal(tokenAddress); + + // Verify token is no longer whitelisted + expect(await swapV0.whitelistedTokens(tokenAddress)).to.be.false; + }); + + it("should revert when adding zero address to whitelist", async function () { + await expect( + swapV0.addTokenToWhitelist(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(swapV0, "ZeroAddressToken"); + }); + + it("should revert when adding already whitelisted token", async function () { + // Use tokenA which is already whitelisted in beforeEach + const tokenAddress = await tokenA.getAddress(); + + await expect( + swapV0.addTokenToWhitelist(tokenAddress) + ).to.be.revertedWithCustomError(swapV0, "TokenAlreadyWhitelisted"); + }); + + it("should revert when removing zero address from whitelist", async function () { + await expect( + swapV0.removeTokenFromWhitelist(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(swapV0, "ZeroAddressToken"); + }); + + it("should revert when removing non-whitelisted token", async function () { + // Create a new token that's not whitelisted + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + const tokenAddress = await tokenC.getAddress(); + + await expect( + swapV0.removeTokenFromWhitelist(tokenAddress) + ).to.be.revertedWithCustomError(swapV0, "TokenNotWhitelisted"); + }); + + it("should revert when non-owner tries to add token to whitelist", async function () { + // Create a new token that's not whitelisted + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + const tokenAddress = await tokenC.getAddress(); + + await expect( + swapV0.connect(signers.alice).addTokenToWhitelist(tokenAddress) + ).to.be.revertedWithCustomError(swapV0, "OwnableUnauthorizedAccount"); + }); + + it("should revert when non-owner tries to remove token from whitelist", async function () { + // Use tokenA which is already whitelisted in beforeEach + const tokenAddress = await tokenA.getAddress(); + + await expect( + swapV0.connect(signers.alice).removeTokenFromWhitelist(tokenAddress) + ).to.be.revertedWithCustomError(swapV0, "OwnableUnauthorizedAccount"); + }); + + it("should reject swap when tokens in path are not whitelisted", async function () { + // Remove tokens from whitelist (they were added in beforeEach) + await swapV0.removeTokenFromWhitelist(await tokenA.getAddress()); + await swapV0.removeTokenFromWhitelist(await tokenB.getAddress()); + + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal(""); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + + // Verify user was refunded (balance unchanged after fees) + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Alice loses transferAmount but gets refund minus fees + expect(balanceCTokenABefore - balanceCTokenAAfter).to.be.lessThan(transferAmount); + expect(balanceCTokenBAfter).to.equal(balanceCTokenBBefore); + }); + + it("should reject swap when only first token in path is whitelisted", async function () { + // Remove tokens from whitelist (they were added in beforeEach), then whitelist only tokenA + await swapV0.removeTokenFromWhitelist(await tokenA.getAddress()); + await swapV0.removeTokenFromWhitelist(await tokenB.getAddress()); + await swapV0.addTokenToWhitelist(await tokenA.getAddress()); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + }); + + it("should reject swap when only last token in path is whitelisted", async function () { + // Remove tokens from whitelist (they were added in beforeEach), then whitelist only tokenB + await swapV0.removeTokenFromWhitelist(await tokenA.getAddress()); + await swapV0.removeTokenFromWhitelist(await tokenB.getAddress()); + await swapV0.addTokenToWhitelist(await tokenB.getAddress()); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + }); + + it("should reject swap with multi-hop path when middle token is not whitelisted", async function () { + // Deploy a third token for multi-hop path + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + + // Create liquidity pools: A-C and C-B + await addLiquidity(signers.alice, router, tokenA, 100_000_000, tokenC, 100_000_000); + await addLiquidity(signers.alice, router, tokenC, 100_000_000, tokenB, 100_000_000); + + const { cToken: cTokenC, wrapper: wrapperC } = await deployConfidentialToken(coordinator, tokenC, signers.alice); + + // TokenA and tokenB are already whitelisted from beforeEach, just don't whitelist tokenC + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Multi-hop path: A -> C -> B (C is not whitelisted) + const path = [await tokenA.getAddress(), await tokenC.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + }); + + it("should allow swap when all tokens in path are whitelisted", async function () { + // Both tokens are already whitelisted in beforeEach + + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(true); + + // Verify balances changed (swap succeeded) + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + expect(balanceCTokenBAfter).to.be.greaterThan(balanceCTokenBBefore); + }); + + it("should allow multi-hop swap when all tokens in path are whitelisted", async function () { + // Deploy a third token for multi-hop path + const tokenC = await deployTestERC20Fixture("TOK_C", 6); + await tokenC.mint(signers.alice, 100_000_000_000); + + // Create liquidity pools: A-C and C-B + await addLiquidity(signers.alice, router, tokenA, 100_000_000, tokenC, 100_000_000); + await addLiquidity(signers.alice, router, tokenC, 100_000_000, tokenB, 100_000_000); + + const { cToken: cTokenC, wrapper: wrapperC } = await deployConfidentialToken(coordinator, tokenC, signers.alice); + + // Whitelist tokenC (tokenA and tokenB are already whitelisted in beforeEach) + await swapV0.addTokenToWhitelist(await tokenC.getAddress()); + + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = await getConfidentialBalance(cTokenB, signers.alice); + + const transferAmount = BigInt(1_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Multi-hop path: A -> C -> B + const path = [await tokenA.getAddress(), await tokenC.getAddress(), await tokenB.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(true); + + // Verify balances changed (swap succeeded) + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + expect(balanceCTokenBAfter).to.be.greaterThan(balanceCTokenBBefore); + }); + + describe.skip("Attack Prevention", function () { + it("should prevent fee bypass attack with fake token (Example 1: avoid wrap fees)", async function () { + // Bob deploys a fake token and creates pools + const fakeToken = await deployTestERC20Fixture("FAKE", 6); + await fakeToken.mint(signers.bob, 100_000_000_000); + + // Create pools: USDC<>fake and WETH<>fake with Bob controlling liquidity + const tokenUSDC = tokenA; // reuse tokenA as USDC + const tokenWETH = tokenB; // reuse tokenB as WETH + + // Mint tokens to Bob so he can provide liquidity + await tokenUSDC.mint(signers.bob, 100_000_000_000); + await tokenWETH.mint(signers.bob, 100_000_000_000); + + await addLiquidity(signers.bob, router, tokenUSDC, 1, fakeToken, 1_000_000); + await addLiquidity(signers.bob, router, fakeToken, 1_000_000, tokenWETH, 10_000_000); // favorable rate for attacker + + // Whitelist legitimate tokens but NOT fake token + await swapV0.addTokenToWhitelist(await tokenUSDC.getAddress()); + await swapV0.addTokenToWhitelist(await tokenWETH.getAddress()); + // fake token is NOT whitelisted + + // Bob wraps 1 wei of USDC + await wrapERC20(coordinator, tokenUSDC, BigInt(1), signers.bob.address, signers.bob); + + const transferAmount = BigInt(1); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.bob.address) + .add64(transferAmount) + .encrypt(); + + // Bob tries to use path [USDC, fake, WETH] to swap 1 wei USDC -> 10 WETH + const path = [await tokenUSDC.getAddress(), await fakeToken.getAddress(), await tokenWETH.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.bob.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.bob)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.bob); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + // Verify swap was rejected due to fake token not being whitelisted + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + }); + + it("should prevent fee bypass attack with fake token (Example 2: avoid unwrap fees)", async function () { + // Bob deploys a fake token + const fakeToken = await deployTestERC20Fixture("FAKE", 6); + await fakeToken.mint(signers.bob, 100_000_000_000); + + // Create pools: WETH<>fake and fake<>USDC with Bob controlling liquidity + const tokenWETH = tokenA; // reuse tokenA as WETH + const tokenUSDC = tokenB; // reuse tokenB as USDC + + // Mint tokens to Bob so he can provide liquidity + await tokenWETH.mint(signers.bob, 100_000_000_000); + await tokenUSDC.mint(signers.bob, 100_000_000_000); + + await addLiquidity(signers.bob, router, tokenWETH, 10_000_000, fakeToken, 1_000_000); + await addLiquidity(signers.bob, router, fakeToken, 1_000_000, tokenUSDC, 1); // 1 wei USDC out + + // Whitelist legitimate tokens but NOT fake token + await swapV0.addTokenToWhitelist(await tokenWETH.getAddress()); + await swapV0.addTokenToWhitelist(await tokenUSDC.getAddress()); + // fake token is NOT whitelisted + + // Bob wraps 10 WETH + await wrapERC20(coordinator, tokenWETH, BigInt(10_000_000), signers.bob.address, signers.bob); + + const transferAmount = BigInt(10_000_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.bob.address) + .add64(transferAmount) + .encrypt(); + + // Bob tries to use path [WETH, fake, USDC] to swap 10 WETH -> 1 wei USDC + // This would let him unwrap 10 WETH with reduced unwrap fee, then recover WETH from liquidity + const path = [await tokenWETH.getAddress(), await fakeToken.getAddress(), await tokenUSDC.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.bob.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.bob)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const swapFinalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.bob); + const swapEvents = getSwapEvent(swapFinalizedReceipt); + + // Verify swap was rejected due to fake token not being whitelisted + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.TOKEN_NOT_WHITELISTED()); + }); + }); + }); + + describe("Security - Malicious Wrapper Attack Prevention", function () { + it("should revert when malicious contract attempts to call onUnwrapFinalizedReceived directly", async function () { + // This test verifies the fix for the vulnerability where an attacker could + // call onUnwrapFinalizedReceived with a malicious contract to steal tokens + + // Setup: Send some tokenA to SwapV0 (simulating leftover tokens) + const swapV0Balance = BigInt(50_000); + await tokenA.connect(signers.alice).transfer(await swapV0.getAddress(), swapV0Balance); + + // Verify SwapV0 has tokens + const balanceBefore = await tokenA.balanceOf(await swapV0.getAddress()); + expect(balanceBefore).to.equal(swapV0Balance); + + // Deploy malicious wrapper attacker + const MaliciousWrapperAttackerFactory = await ethers.getContractFactory("MaliciousWrapperAttacker"); + const maliciousWrapper = await MaliciousWrapperAttackerFactory.connect(signers.bob).deploy( + await tokenA.getAddress(), + await swapV0.getAddress() + ); + + // Prepare attack parameters + // Path with tokenB as first element (doesn't match tokenA from originalToken()) + // This would trigger checkPath to return false BEFORE wrapper verification in vulnerable code + const path = [await tokenB.getAddress(), await tokenA.getAddress()]; + const amountIn = BigInt(10_000); + + // Attempt the attack - should revert with UnknownWrapper error + // The fix ensures wrapper validation happens at the START of checkPath + await expect( + maliciousWrapper.connect(signers.bob).executeAttack( + amountIn, + await router.getAddress(), + path + ) + ).to.be.revertedWithCustomError(swapV0, "UnknownWrapper"); + + // Verify no tokens were stolen + const swapV0BalanceAfter = await tokenA.balanceOf(await swapV0.getAddress()); + expect(swapV0BalanceAfter).to.equal(swapV0Balance); + + const stolenAmount = await maliciousWrapper.stolenAmount(); + expect(stolenAmount).to.equal(0); + }); + }); + + describe("Governance - Rescue Tokens", function () { + it("should allow owner to rescue stuck ERC20 tokens", async function () { + // Get the current owner + const ownerAddress = await swapV0.owner(); + const owner = await ethers.getSigner(ownerAddress); + + // Send some tokens to SwapV0 (simulating stuck tokens) + const stuckAmount = BigInt(100_000); + await tokenA.connect(signers.alice).transfer(await swapV0.getAddress(), stuckAmount); + + // Verify SwapV0 has tokens + const swapV0BalanceBefore = await tokenA.balanceOf(await swapV0.getAddress()); + expect(swapV0BalanceBefore).to.equal(stuckAmount); + + // Owner rescues tokens to alice + const aliceBalanceBefore = await tokenA.balanceOf(signers.alice.address); + const tx = await swapV0.connect(owner).rescueTokens( + await tokenA.getAddress(), + signers.alice.address, + stuckAmount + ); + + // Verify event was emitted + await expect(tx) + .to.emit(swapV0, "TokensRescued") + .withArgs(await tokenA.getAddress(), signers.alice.address, stuckAmount); + + // Verify tokens were transferred + const swapV0BalanceAfter = await tokenA.balanceOf(await swapV0.getAddress()); + expect(swapV0BalanceAfter).to.equal(0); + + const aliceBalanceAfter = await tokenA.balanceOf(signers.alice.address); + expect(aliceBalanceAfter - aliceBalanceBefore).to.equal(stuckAmount); + }); + + it("should allow owner to rescue stuck ETH", async function () { + // Get the current owner + const ownerAddress = await swapV0.owner(); + const owner = await ethers.getSigner(ownerAddress); + + // Send some ETH to SwapV0 (simulating stuck ETH) + const stuckAmount = ethers.parseEther("1.0"); + await signers.alice.sendTransaction({ + to: await swapV0.getAddress(), + value: stuckAmount, + }); + + // Verify SwapV0 has ETH + const swapV0BalanceBefore = await ethers.provider.getBalance(await swapV0.getAddress()); + expect(swapV0BalanceBefore).to.be.gte(stuckAmount); + + // Owner rescues ETH to alice + const aliceBalanceBefore = await ethers.provider.getBalance(signers.alice.address); + const tx = await swapV0.connect(owner).rescueTokens( + ethers.ZeroAddress, + signers.alice.address, + stuckAmount + ); + + // Verify event was emitted + await expect(tx) + .to.emit(swapV0, "TokensRescued") + .withArgs(ethers.ZeroAddress, signers.alice.address, stuckAmount); + + // Verify ETH was transferred + const swapV0BalanceAfter = await ethers.provider.getBalance(await swapV0.getAddress()); + expect(swapV0BalanceBefore - swapV0BalanceAfter).to.equal(stuckAmount); + + const aliceBalanceAfter = await ethers.provider.getBalance(signers.alice.address); + // Alice balance should increase by stuckAmount + expect(aliceBalanceAfter - aliceBalanceBefore).to.equal(stuckAmount); + }); + + it("should revert when non-owner tries to rescue tokens", async function () { + // Send tokens to SwapV0 + await tokenA.connect(signers.alice).transfer(await swapV0.getAddress(), BigInt(10_000)); + + // Non-owner (bob) tries to rescue tokens - should revert + await expect( + swapV0.connect(signers.bob).rescueTokens( + await tokenA.getAddress(), + signers.bob.address, + BigInt(10_000) + ) + ).to.be.revertedWithCustomError(swapV0, "OwnableUnauthorizedAccount"); + }); + + it("should revert when rescuing to zero address", async function () { + // Get the current owner + const ownerAddress = await swapV0.owner(); + const owner = await ethers.getSigner(ownerAddress); + + // Send tokens to SwapV0 + await tokenA.connect(signers.alice).transfer(await swapV0.getAddress(), BigInt(10_000)); + + // Try to rescue to zero address - should revert + await expect( + swapV0.connect(owner).rescueTokens( + await tokenA.getAddress(), + ethers.ZeroAddress, + BigInt(10_000) + ) + ).to.be.revertedWithCustomError(swapV0, "ZeroAddressRecipient"); + }); + + it("should allow rescuing partial amount", async function () { + // Get the current owner + const ownerAddress = await swapV0.owner(); + const owner = await ethers.getSigner(ownerAddress); + + // Send tokens to SwapV0 + const stuckAmount = BigInt(100_000); + await tokenA.connect(signers.alice).transfer(await swapV0.getAddress(), stuckAmount); + + // Rescue only half + const rescueAmount = stuckAmount / BigInt(2); + await swapV0.connect(owner).rescueTokens( + await tokenA.getAddress(), + signers.alice.address, + rescueAmount + ); + + // Verify partial rescue + const swapV0BalanceAfter = await tokenA.balanceOf(await swapV0.getAddress()); + expect(swapV0BalanceAfter).to.equal(stuckAmount - rescueAmount); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/SwapETH.ts b/protocol-contracts/confidential-wrapper/test/SwapETH.ts new file mode 100644 index 0000000000..2a64d6fbd7 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/SwapETH.ts @@ -0,0 +1,605 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { AdminProvider, DeploymentCoordinator, RegulatedERC7984Upgradeable, SwapV0, TestERC20, UniswapV2Factory, UniswapV2Pair, UniswapV2Router02, WETH9, WrapperUpgradeable } from "../types"; +import { deploySwapV0Fixture, deployTestERC20Fixture, deployUniswapFactoryFixture, deployWrapperFixture } from "./fixtures"; +import { getSigners, Signers } from "./signers"; +import { ethers, fhevm } from "hardhat"; +import { deployConfidentialToken, deployConfidentialETH, getConfidentialBalance, getFeeManager, getSwapEvent, getUnwrapFee, getUnwrapFinalizedEvent, getUnwrapStartedEvent, wrapERC20, wrapETH, finalizeUnwrapFromReceipt } from "./utils"; +import { expect } from "chai"; + + +async function addLiquidityETH( + deployer: HardhatEthersSigner, + router: UniswapV2Router02, + token: TestERC20, + amountToken: number | bigint, + amountETH: number | bigint, +) { + await token.connect(deployer).approve(router.target, 100_000_000_000); + await router.connect(deployer).addLiquidityETH( + token.target, + amountToken, + 0, + 0, + deployer, + Math.floor(Date.now() / 1000) + 6000, + { value: amountETH } + ); + + const factoryAddress = await router.factory(); + const factory = await ethers.getContractAt("UniswapV2Factory", factoryAddress); + const wethAddress = await router.WETH(); + const pairAddress = await factory.getPair(token.target, wethAddress); + return await ethers.getContractAt("UniswapV2Pair", pairAddress); +} + +async function getSwapFinalizedReceipt(transferReceipt: any, wrapper: WrapperUpgradeable, signer: HardhatEthersSigner) { + return await finalizeUnwrapFromReceipt(transferReceipt, wrapper, signer); +} + + +async function getCTokensFromPath(coordinator: DeploymentCoordinator, router: UniswapV2Router02, path: string[]) { + const tokenInAddress = path[0] === await router.WETH() ? ethers.ZeroAddress : path[0]; + const cTokenInAddress = await coordinator.deployedConfidentialTokens(tokenInAddress); + const cTokenIn = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenInAddress); + + const tokenOutAddress = path[path.length - 1] === await router.WETH() ? ethers.ZeroAddress : path[path.length - 1]; + const cTokenOutAddress = await coordinator.deployedConfidentialTokens(tokenOutAddress); + const cTokenOut = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenOutAddress); + + return {cTokenIn, cTokenOut}; +} + + +async function getAmountOut(coordinator: DeploymentCoordinator, router: UniswapV2Router02, path: string[], amount: bigint) { + const { cTokenIn, cTokenOut } = await getCTokensFromPath(coordinator, router, path); + + const feeManager = await getFeeManager(coordinator); + const unwrapFee = await feeManager.getUnwrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + + const swapInAmount = (amount - unwrapFee) * await cTokenIn.rate(); + const swapOutAmounts = await router.getAmountsOut(swapInAmount, path); + const swapOutAmount = swapOutAmounts[swapOutAmounts.length - 1]; + + const wrapFee = await feeManager.getWrapFee(swapOutAmount, ethers.ZeroAddress, ethers.ZeroAddress); + + return (swapOutAmount - wrapFee) / await cTokenOut.rate(); +} + + +async function getRefund(coordinator: DeploymentCoordinator, router: UniswapV2Router02, path: string[], amount: bigint) { + const { cTokenIn } = await getCTokensFromPath(coordinator, router, path); + + const feeManager = await getFeeManager(coordinator); + const unwrapFee = await feeManager.getUnwrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + + const swapInAmount = (amount - unwrapFee) * await cTokenIn.rate(); + + const wrapFee = await feeManager.getWrapFee(swapInAmount, ethers.ZeroAddress, ethers.ZeroAddress); + + return (swapInAmount - wrapFee) / await cTokenIn.rate(); +} + + +describe("SwapV0 ETH Support", function () { + let signers: Signers; + let tokenA: TestERC20; + let tokenB: TestERC20; + let coordinator: DeploymentCoordinator; + let cTokenA: RegulatedERC7984Upgradeable; + let wrapperA: WrapperUpgradeable; + let cTokenB: RegulatedERC7984Upgradeable; + let wrapperB: WrapperUpgradeable; + let cETH: RegulatedERC7984Upgradeable; + let wrapperETH: WrapperUpgradeable; + let router: UniswapV2Router02; + let factory: UniswapV2Factory; + let weth: WETH9; + let pair: UniswapV2Pair; + let swapV0: SwapV0; + let adminProvider: AdminProvider; + + beforeEach(async function () { + signers = await getSigners(); + ({ router, factory, weth } = await deployUniswapFactoryFixture(signers.admin)); + + // Deploy test ERC20 tokens + tokenA = await deployTestERC20Fixture("TOK_A", 6); + await tokenA.mint(signers.alice, 100_000_000_000); + + tokenB = await deployTestERC20Fixture("TOK_B", 6); + await tokenB.mint(signers.alice, 100_000_000_000); + + // Add liquidity: tokenA <-> ETH (via WETH) + await addLiquidityETH(signers.alice, router, tokenA, 100_000_000, ethers.parseEther("10")); + + // Add liquidity: tokenB <-> ETH (via WETH) + await addLiquidityETH(signers.alice, router, tokenB, 200_000_000, ethers.parseEther("10")); + + ({ coordinator, adminProvider } = await deployWrapperFixture(signers)); + + // Deploy confidential tokenA + ({ cToken: cTokenA, wrapper: wrapperA } = await deployConfidentialToken(coordinator, tokenA, signers.alice)); + + // Deploy confidential tokenB + ({ cToken: cTokenB, wrapper: wrapperB } = await deployConfidentialToken(coordinator, tokenB, signers.alice)); + + // Deploy confidential ETH (originalToken = address(0)) + ({ cEth: cETH, wrapper: wrapperETH } = await deployConfidentialETH(coordinator, signers.alice)); + + // Wrap some tokens for alice + await wrapERC20(coordinator, tokenA, BigInt(100_000), signers.alice.address, signers.alice); + + // Wrap some ETH for alice + const wrapAmount = ethers.parseEther("1"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + ({ swapV0 } = await deploySwapV0Fixture(coordinator)); + + // Whitelist the router + await swapV0.addRouterToWhitelist(await router.getAddress()); + + // Whitelist tokens (tokenA, tokenB, and WETH) + await swapV0.addTokenToWhitelist(await tokenA.getAddress()); + await swapV0.addTokenToWhitelist(await tokenB.getAddress()); + await swapV0.addTokenToWhitelist(await weth.getAddress()); + }); + + afterEach(async function () { + if (this.currentTest.skipCleanup) { + return; // Skip afterEach logic + } + + // Verify no ETH stuck in SwapV0 + const swapBalance = await ethers.provider.getBalance(await swapV0.getAddress()); + expect(swapBalance).to.equal(0); + + // Verify no tokenA stuck in SwapV0 + const tokenABalance = await tokenA.balanceOf(await swapV0.getAddress()); + expect(tokenABalance).to.equal(0); + + // Verify no tokenB stuck in SwapV0 + const tokenBBalance = await tokenB.balanceOf(await swapV0.getAddress()); + expect(tokenBBalance).to.equal(0); + }); + + describe("ETH → Token Swaps (cETH → cToken)", function () { + it("should swap cETH for cTokenA", async function () { + const balanceCETHBefore = await getConfidentialBalance(cETH, signers.alice); + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + + // Transfer a small amount of cETH (remember: cETH uses internal representation) + // Alice has 990000 cETH tokens, let's transfer 1000 + const transferAmount = BigInt(1000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cETH.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Path: WETH -> tokenA (Uniswap uses WETH for ETH swaps) + const path = [await weth.getAddress(), await tokenA.getAddress()]; + const expectedAmountOut = await getAmountOut(coordinator, router, path, transferAmount); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cETH.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperETH.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperETH, signers.alice); + + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizedReceipt); + expect(unwrapFinalizedEvents).to.have.length(1); + + // Verify Swap event was emitted with success + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + const swapEvent = swapEvents[0]; + expect(swapEvent.args.success).to.equal(true); + expect(swapEvent.args.path).to.deep.equal(path); + expect(swapEvent.args.errorReasonString).to.equal(""); + expect(swapEvent.args.errorLowLevelData).to.equal("0x"); + + const balanceCETHAfter = await getConfidentialBalance(cETH, signers.alice); + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + + + // Verify balances changed (swap succeeded) + expect(balanceCETHBefore - balanceCETHAfter).to.equal(transferAmount); + expect(balanceCTokenAAfter - balanceCTokenABefore).to.equal(expectedAmountOut); + }); + + it("should refund cETH when ETH → Token swap fails", async function () { + const balanceAliceCETHBefore = await getConfidentialBalance(cETH, signers.alice); + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + + const transferAmount = BigInt(1000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cETH.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await weth.getAddress(), await tokenA.getAddress()]; + const expectedRefund = await getRefund(coordinator, router, path, transferAmount); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) - 1, // Expired deadline to force failure + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, // Bob is refundTo + callbackData, + ] + ); + + const transferTx = await cETH.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperETH.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperETH, signers.alice); + + // Verify Swap event shows failure + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal("UniswapV2Router: EXPIRED"); + + const balanceAliceCETHAfter = await getConfidentialBalance(cETH, signers.alice); + const balanceBobCETHAfter = await getConfidentialBalance(cETH, signers.bob); + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + + // Alice's balance decreases by the transfer amount + expect(balanceAliceCETHBefore - balanceAliceCETHAfter).to.equal(transferAmount); + + // Bob receives a refund (he finalized the unwrap) + expect(balanceBobCETHAfter).to.equal(expectedRefund); + + // No cTokenA received (swap failed) + expect(balanceCTokenAAfter).to.equal(balanceCTokenABefore); + }); + + it("should reject ETH → Token swap when path[0] is not WETH", async function () { + const transferAmount = BigInt(1000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cETH.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Invalid path: should be WETH -> tokenA, but using tokenA -> tokenA + const invalidPath = [await tokenA.getAddress(), await tokenA.getAddress()]; + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + invalidPath, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cETH.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperETH.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperETH, signers.alice); + + // Verify Swap event shows failure + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorLowLevelData).to.equal(await swapV0.INPUT_PATH_IS_NOT_UNDERLYING()); + }); + }); + + describe("Token → ETH Swaps (cToken → cETH)", function () { + it("should swap cTokenA for cETH", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCETHBefore = await getConfidentialBalance(cETH, signers.alice); + + const transferAmount = BigInt(10_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Path: tokenA -> WETH (Uniswap uses WETH for ETH swaps) + const path = [await tokenA.getAddress(), await weth.getAddress()]; + const expectedAmountOut = await getAmountOut(coordinator, router, path, transferAmount); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizedReceipt); + expect(unwrapFinalizedEvents).to.have.length(1); + + // Verify Swap event was emitted with success + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + const swapEvent = swapEvents[0]; + expect(swapEvent.args.success).to.equal(true); + expect(swapEvent.args.path).to.deep.equal(path); + expect(swapEvent.args.errorReasonString).to.equal(""); + expect(swapEvent.args.errorLowLevelData).to.equal("0x"); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCETHAfter = await getConfidentialBalance(cETH, signers.alice); + + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + expect(balanceCETHAfter - balanceCETHBefore).to.equal(expectedAmountOut); + }); + + it("should refund cTokenA when Token → ETH swap fails", async function () { + const balanceAliceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenABefore = 0n; + const balanceCETHBefore = await getConfidentialBalance(cETH, signers.alice); + + const transferAmount = BigInt(10_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const path = [await tokenA.getAddress(), await weth.getAddress()]; + const expectedRefund = await getRefund(coordinator, router, path, transferAmount); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) - 1, // Expired deadline to force failure + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.bob.address, // Bob is refundTo + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + // Bob finalizes the unwrap - refund should go to Bob + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + // Verify Swap event shows failure + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + expect(swapEvents[0].args.success).to.equal(false); + expect(swapEvents[0].args.errorReasonString).to.equal("UniswapV2Router: EXPIRED"); + + const balanceAliceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceBobCTokenAAfter = await getConfidentialBalance(cTokenA, signers.bob); + const balanceCETHAfter = await getConfidentialBalance(cETH, signers.alice); + + // Alice's balance decreases by the transfer amount + expect(balanceAliceCTokenABefore - balanceAliceCTokenAAfter).to.equal(transferAmount); + + // Bob receives the refund (he finalized the unwrap) + expect(balanceBobCTokenAAfter - balanceBobCTokenABefore).to.equal(expectedRefund); + + // No cETH received (swap failed) + expect(balanceCETHAfter - balanceCETHBefore).to.equal(0); + }); + }); + + describe("Path Validation for ETH Swaps", function () { + it("should accept valid ETH input path (WETH as path[0])", async function () { + const path = [await weth.getAddress(), await tokenA.getAddress()]; + + const [isValid, errorString, errorData] = await swapV0.checkPath( + wrapperETH, + await router.getAddress(), + path + ); + + expect(isValid).to.equal(true); + expect(errorString).to.equal(""); + expect(errorData).to.equal("0x"); + }); + + it("should accept valid ETH output path (WETH as path[last])", async function () { + const path = [await tokenA.getAddress(), await weth.getAddress()]; + + const [isValid, errorString, errorData] = await swapV0.checkPath( + wrapperA, + await router.getAddress(), + path + ); + + expect(isValid).to.equal(true); + expect(errorString).to.equal(""); + expect(errorData).to.equal("0x"); + }); + + it("should reject ETH input path when path[0] is not WETH", async function () { + // Try to use tokenA as path[0] when underlying is ETH (address(0)) + const path = [await tokenA.getAddress(), await weth.getAddress()]; + + const [isValid, errorString, errorData] = await swapV0.checkPath( + wrapperETH, // ETH wrapper + await router.getAddress(), + path + ); + + expect(isValid).to.equal(false); + expect(errorData).to.equal(await swapV0.INPUT_PATH_IS_NOT_UNDERLYING()); + }); + + it("should verify SwapV0 has receive() function for ETH", async function () { + // Since we're sending funds directly to the swapper + this.test.skipCleanup = true; + + // Send ETH directly to SwapV0 to verify it can receive ETH + const sendAmount = ethers.parseEther("0.1"); + const balanceBefore = await ethers.provider.getBalance(await swapV0.getAddress()); + + await signers.alice.sendTransaction({ + to: await swapV0.getAddress(), + value: sendAmount + }); + + const balanceAfter = await ethers.provider.getBalance(await swapV0.getAddress()); + expect(balanceAfter - balanceBefore).to.equal(sendAmount); + }); + }); + + describe("Multi-Hop Swaps with ETH", function () { + it("should swap cTokenA -> ETH -> cTokenB", async function () { + const balanceCTokenABefore = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBBefore = 0n; + + const transferAmount = BigInt(10_000); + const encryptedTransferAmount = await fhevm + .createEncryptedInput(await cTokenA.getAddress(), signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Path: tokenA -> WETH -> tokenB (multi-hop through ETH) + const path = [ + await tokenA.getAddress(), + await weth.getAddress(), + await tokenB.getAddress() + ]; + const expectedAmountOut = await getAmountOut(coordinator, router, path, transferAmount); + + const abiCoder = new ethers.AbiCoder(); + const callbackData = abiCoder.encode( + ["tuple(address, uint256, address[], uint256, address)"], + [[ + await router.getAddress(), + 0, + path, + Math.floor(Date.now() / 1000) + 6000, + signers.alice.address, + ]] + ); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [ + await swapV0.getAddress(), + signers.alice.address, + callbackData, + ] + ); + + const transferTx = await cTokenA.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapperA.getAddress(), + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + data, + ); + const transferReceipt = await transferTx.wait(); + + const finalizedReceipt = await getSwapFinalizedReceipt(transferReceipt, wrapperA, signers.alice); + + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizedReceipt); + expect(unwrapFinalizedEvents).to.have.length(1); + + // Verify Swap event was emitted with success + const swapEvents = getSwapEvent(finalizedReceipt); + expect(swapEvents).to.have.length(1); + const swapEvent = swapEvents[0]; + expect(swapEvent.args.success).to.equal(true); + expect(swapEvent.args.path).to.deep.equal(path); + expect(swapEvent.args.errorReasonString).to.equal(""); + expect(swapEvent.args.errorLowLevelData).to.equal("0x"); + + const balanceCTokenAAfter = await getConfidentialBalance(cTokenA, signers.alice); + const balanceCTokenBAfter = await getConfidentialBalance(cTokenB, signers.alice); + + // Verify balances changed correctly + expect(balanceCTokenABefore - balanceCTokenAAfter).to.equal(transferAmount); + expect(balanceCTokenBAfter - balanceCTokenBBefore).to.equal(expectedAmountOut); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/TransferBatcher.ts b/protocol-contracts/confidential-wrapper/test/TransferBatcher.ts new file mode 100644 index 0000000000..e9fce369b1 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/TransferBatcher.ts @@ -0,0 +1,946 @@ +import { ethers, fhevm } from "hardhat"; +import { RegulatedERC7984Upgradeable, ERC7984TransferBatcher, AdminProvider } from "../types"; +import { expect } from "chai"; +import { getBatchTransferEvent, getConfidentialBalance, getRetryTransferEvent, getBatchTransferFee } from "./utils"; +import { getSigners, Signers } from "./signers"; +import { deployConfidentialErc20Fixture, deployTransferBatcherFixture } from "./fixtures"; + + +describe("TransferBatcher", function () { + let signers: Signers; + let cErc20: RegulatedERC7984Upgradeable; + let cErc20Address: string; + let transferBatcher: ERC7984TransferBatcher; + let transferBatcherAddress: string; + let adminProvider: AdminProvider; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async () => { + ({ cErc20, cErc20Address, adminProvider } = await deployConfidentialErc20Fixture(signers)); + await cErc20.grantRole(await cErc20.WRAPPER_ROLE(), signers.deployer); + ({transferBatcher, transferBatcherAddress} = await deployTransferBatcherFixture(signers.deployer, adminProvider)); + }); + + describe("confidentialBatchTransfer", function () { + it("should batch transfer without fees", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const aliceBalanceBefore = await getConfidentialBalance(cErc20, signers.alice); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const initTxId = await cErc20.nextTxId(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .add64(transferAmount * 2) + .add64(transferAmount * 3) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[1], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.delta.address, + "encryptedAmount": encryptedTransferAmount.handles[2], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ] + const nTransfers = BigInt(transferData.length); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const batchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: batchTransferFee }); + const receipt = await batchTransfer.wait(); + + const batchTransferEvents = getBatchTransferEvent(receipt); + expect(batchTransferEvents.length).to.equal(1); + const batchTransferEvent = batchTransferEvents[0]; + expect(batchTransferEvent.args[0]).to.equal(cErc20Address); + expect(batchTransferEvent.args[1]).to.equal(signers.alice.address); + expect(batchTransferEvent.args[2]).to.equal(initTxId); + expect(batchTransferEvent.args[3]).to.equal(initTxId + nTransfers - BigInt(1)); + + expect(await getConfidentialBalance(cErc20, signers.alice)).to.equal(aliceBalanceBefore - (BigInt(transferAmount * 6))); + expect(await getConfidentialBalance(cErc20, signers.bob)).to.equal(transferAmount); + expect(await getConfidentialBalance(cErc20, signers.charlie)).to.equal(transferAmount * 2); + expect(await getConfidentialBalance(cErc20, signers.delta)).to.equal(transferAmount * 3); + }); + + it("should allow retry by original sender", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Original transfer + const originalTransferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const originalTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const originalBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, originalTransferData, { value: batchTransferFee }); + await originalBatchTransfer.wait(); + + // Verify original transaction was recorded + expect(await transferBatcher.txIdToSender(cErc20Address, originalTxId)).to.equal(signers.alice.address); + + // Retry the same transfer + const encryptedRetryAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const retryTransferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedRetryAmount.handles[0], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(originalTxId), + } + ]; + + const retryTxId = await cErc20.nextTxId(); + const retryBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, retryTransferData, { value: batchTransferFee }); + const retryReceipt = await retryBatchTransfer.wait(); + + // Check retry event was emitted + const retryEvents = getRetryTransferEvent(retryReceipt); + expect(retryEvents.length).to.equal(1); + const retryEvent = retryEvents[0]; + expect(retryEvent.args[0]).to.equal(cErc20Address); // cToken + expect(retryEvent.args[1]).to.equal(signers.alice.address); // sender + expect(retryEvent.args[2]).to.equal(originalTxId); // originalTxId + expect(retryEvent.args[3]).to.equal(retryTxId); // retryTxId + + // Verify retry transaction was also recorded + expect(await transferBatcher.txIdToSender(cErc20Address, retryTxId)).to.equal(signers.alice.address); + + // Verify both transfers went through (bob should have double the amount) + expect(await getConfidentialBalance(cErc20, signers.bob)).to.equal(transferAmount * 2); + }); + + it("should reject retry by non-original sender", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperatorAlice = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperatorAlice.wait(); + + const setOperatorBob = await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperatorBob.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Original transfer by Alice + const originalTransferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const originalTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const originalBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, originalTransferData, { value: batchTransferFee }); + await originalBatchTransfer.wait(); + + // Try to retry as Bob (should fail) + const encryptedRetryAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + const retryTransferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedRetryAmount.handles[0], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(originalTxId), + } + ]; + + await expect( + transferBatcher.connect(signers.bob).confidentialBatchTransfer(cErc20Address, signers.bob.address, retryTransferData, { value: batchTransferFee }) + ).to.be.revertedWithCustomError(transferBatcher, "OnlyOriginalSenderCanRetry"); + }); + + it("should handle multiple retries in same batch", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .add64(transferAmount * 2) + .encrypt(); + + // First batch of original transfers + const originalTransferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[1], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const firstTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const originalBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, originalTransferData, { value: batchTransferFee }); + await originalBatchTransfer.wait(); + + const secondTxId = firstTxId + BigInt(1); + + // Retry both transfers in a single batch + const encryptedRetryAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .add64(transferAmount * 2) + .encrypt(); + + const retryTransferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedRetryAmount.handles[0], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(firstTxId), + }, + { + "to": signers.charlie.address, + "encryptedAmount": encryptedRetryAmount.handles[1], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(secondTxId), + } + ]; + + const retryBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, retryTransferData, { value: batchTransferFee }); + const retryReceipt = await retryBatchTransfer.wait(); + + // Check that two retry events were emitted + const retryEvents = getRetryTransferEvent(retryReceipt); + expect(retryEvents.length).to.equal(2); + + // Verify the events contain correct information + expect(retryEvents[0].args[2]).to.equal(firstTxId); // originalTxId + expect(retryEvents[1].args[2]).to.equal(secondTxId); // originalTxId + + // Verify balances show both original and retry transfers went through + expect(await getConfidentialBalance(cErc20, signers.bob)).to.equal(transferAmount * 2); + expect(await getConfidentialBalance(cErc20, signers.charlie)).to.equal(transferAmount * 4); + }); + + it("should reject retry for non-existent transaction", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const nonExistentTxId = 999999; + const retryTransferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": nonExistentTxId, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, retryTransferData, { value: batchTransferFee }) + ).to.be.revertedWithCustomError(transferBatcher, "OnlyOriginalSenderCanRetry"); + }); + + it("should reject calls with insufficient fee", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + const insufficientFee = batchTransferFee - BigInt(1); + + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: insufficientFee }) + ).to.be.revertedWithCustomError(transferBatcher, "InsufficientFee"); + }); + + it("should reject calls with excess fee", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + const excessFee = batchTransferFee + BigInt(1); + + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: excessFee }) + ).to.be.revertedWithCustomError(transferBatcher, "InsufficientFee"); + }); + + it("should prevent cross-token transaction ID collision", async function () { + // Deploy a second confidential token + const { cErc20: cErc20B, cErc20Address: cErc20AddressB } = await deployConfidentialErc20Fixture(signers); + await cErc20B.grantRole(await cErc20B.WRAPPER_ROLE(), signers.deployer); + + // Alias for clarity + const cErc20A = cErc20; + const cErc20AddressA = cErc20Address; + + // Mint tokens for alice in token A only + await cErc20A.mint(signers.alice, 100_000_000); + + // Setup operator for token A + await cErc20A.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + + const transferAmount = 1_000; + + // Transfer with token A - this will use txId = 1 + const encryptedAmountA = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferDataA = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedAmountA.handles[0], + "inputProof": encryptedAmountA.inputProof, + "retryFor": 0, + } + ]; + + const txIdA = await cErc20A.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20AddressA, signers.alice.address, transferDataA, { value: batchTransferFee }); + + // Verify alice is recorded as sender for token A's txId + expect(await transferBatcher.txIdToSender(cErc20AddressA, txIdA)).to.equal(signers.alice.address); + + // Now bob transfers with token B - this will also use txId = 1 (both start fresh) + await cErc20B.mint(signers.bob, 100_000_000); + await cErc20B.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + + const encryptedAmountB = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + const transferDataB = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedAmountB.handles[0], + "inputProof": encryptedAmountB.inputProof, + "retryFor": 0, + } + ]; + + const txIdB = await cErc20B.nextTxId(); + expect(txIdB).to.equal(txIdA); // Both tokens have the same txId = 1 + + await transferBatcher.connect(signers.bob).confidentialBatchTransfer(cErc20AddressB, signers.bob.address, transferDataB, { value: batchTransferFee }); + + // Verify bob is recorded as sender for token B's txId + expect(await transferBatcher.txIdToSender(cErc20AddressB, txIdB)).to.equal(signers.bob.address); + + // CRITICAL: Verify alice's entry for token A was NOT overwritten + expect(await transferBatcher.txIdToSender(cErc20AddressA, txIdA)).to.equal(signers.alice.address); + + // Verify alice can still retry her original transaction on token A + const encryptedRetryA = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const retryDataA = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedRetryA.handles[0], + "inputProof": encryptedRetryA.inputProof, + "retryFor": Number(txIdA), + } + ]; + + // This should succeed - alice should still be able to retry her transaction + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20AddressA, signers.alice.address, retryDataA, { value: batchTransferFee }) + ).to.not.be.reverted; + + // Verify bob can retry his transaction on token B + const encryptedRetryB = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + const retryDataB = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedRetryB.handles[0], + "inputProof": encryptedRetryB.inputProof, + "retryFor": Number(txIdB), + } + ]; + + await expect( + transferBatcher.connect(signers.bob).confidentialBatchTransfer(cErc20AddressB, signers.bob.address, retryDataB, { value: batchTransferFee }) + ).to.not.be.reverted; + + // Verify alice CANNOT retry bob's transaction on token B (cross-token protection) + const encryptedAliceRetryB = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const aliceRetryDataB = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedAliceRetryB.handles[0], + "inputProof": encryptedAliceRetryB.inputProof, + "retryFor": Number(txIdB), + } + ]; + + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20AddressB, signers.alice.address, aliceRetryDataB, { value: batchTransferFee }) + ).to.be.revertedWithCustomError(transferBatcher, "OnlyOriginalSenderCanRetry"); + }); + + it("should revert with FeeTransferFailed when fee transfer fails", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Deploy RejectEth contract + const RejectEth = await ethers.getContractFactory("RejectEth"); + const rejectEth = await RejectEth.deploy(); + await rejectEth.waitForDeployment(); + const rejectEthAddress = await rejectEth.getAddress(); + + // Set fee recipient to RejectEth contract + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const originalFeeRecipient = await feeManager.getFeeRecipient(); + await feeManager.setFeeRecipient(rejectEthAddress); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + + // Should revert with FeeTransferFailed error + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: batchTransferFee }) + ).to.be.revertedWithCustomError(transferBatcher, "FeeTransferFailed"); + + // Restore original fee recipient + await feeManager.setFeeRecipient(originalFeeRecipient); + }); + + it("should revert when transfers array is empty", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Empty transfer array + const emptyTransferData: any[] = []; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + + // Should revert with EmptyTransferArray error + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, emptyTransferData, { value: batchTransferFee }) + ).to.be.revertedWithCustomError(transferBatcher, "EmptyTransferArray"); + }); + + it("should emit correct txId range for single transfer", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const expectedStartTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const batchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: batchTransferFee }); + const receipt = await batchTransfer.wait(); + + const batchTransferEvents = getBatchTransferEvent(receipt); + expect(batchTransferEvents.length).to.equal(1); + + const batchTransferEvent = batchTransferEvents[0]; + expect(batchTransferEvent.args[2]).to.equal(expectedStartTxId); // startTxId + expect(batchTransferEvent.args[3]).to.equal(expectedStartTxId); // endTxId should equal startTxId for single transfer + }); + + it("should emit correct txId range for multiple transfers", async function () { + const mint = await cErc20.mint(signers.alice, 100_000_000); + await mint.wait(); + + const setOperator = await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .add64(transferAmount * 2) + .add64(transferAmount * 3) + .encrypt(); + + const transferData = [ + { + "to": signers.bob.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[1], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.delta.address, + "encryptedAmount": encryptedTransferAmount.handles[2], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const expectedStartTxId = await cErc20.nextTxId(); + const expectedEndTxId = expectedStartTxId + BigInt(2); // 3 transfers, so endTxId = startTxId + 2 + const batchTransferFee = await getBatchTransferFee(adminProvider); + const batchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer(cErc20Address, signers.alice.address, transferData, { value: batchTransferFee }); + const receipt = await batchTransfer.wait(); + + const batchTransferEvents = getBatchTransferEvent(receipt); + expect(batchTransferEvents.length).to.equal(1); + + const batchTransferEvent = batchTransferEvents[0]; + expect(batchTransferEvent.args[2]).to.equal(expectedStartTxId); // startTxId + expect(batchTransferEvent.args[3]).to.equal(expectedEndTxId); // endTxId + + // Verify range is valid: endTxId >= startTxId + expect(batchTransferEvent.args[3]).to.be.gte(batchTransferEvent.args[2]); + }); + }); + + describe("Operator functionality", function () { + it("should allow operator to perform batch transfer on behalf of token holder", async function () { + // Mint tokens to Bob + const mint = await cErc20.mint(signers.bob, 100_000_000); + await mint.wait(); + + const bobBalanceBefore = await getConfidentialBalance(cErc20, signers.bob); + + // Bob approves Alice as operator + const setOperator = await cErc20.connect(signers.bob).setOperator(signers.alice.address, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + // Alice also needs to be an operator for the batcher to perform transfers + const setBatcherOperator = await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setBatcherOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const initTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + + // Alice (operator) performs batch transfer on behalf of Bob + const batchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, // Transfer from Bob's account + transferData, + { value: batchTransferFee } + ); + const receipt = await batchTransfer.wait(); + + // Verify the event shows Bob as the sender (not Alice) + const batchTransferEvents = getBatchTransferEvent(receipt); + expect(batchTransferEvents.length).to.equal(1); + const batchTransferEvent = batchTransferEvents[0]; + expect(batchTransferEvent.args[0]).to.equal(cErc20Address); + expect(batchTransferEvent.args[1]).to.equal(signers.bob.address); // Bob is the sender + expect(batchTransferEvent.args[2]).to.equal(initTxId); + + // Verify txIdToSender tracks Bob (token owner), not Alice (operator) + expect(await transferBatcher.txIdToSender(cErc20Address, initTxId)).to.equal(signers.bob.address); + + // Verify balances: Bob's balance decreased, Charlie received tokens + expect(await getConfidentialBalance(cErc20, signers.bob)).to.equal(bobBalanceBefore - BigInt(transferAmount)); + expect(await getConfidentialBalance(cErc20, signers.charlie)).to.equal(transferAmount); + }); + + it("should reject batch transfer when caller is not an approved operator", async function () { + // Mint tokens to Bob + const mint = await cErc20.mint(signers.bob, 100_000_000); + await mint.wait(); + + // Alice is NOT approved as operator for Bob + const setOperator = await cErc20.connect(signers.bob).setOperator(signers.alice.address, Math.floor(Date.now() / 1000) - 6000); + await setOperator.wait(); + + // Bob still needs to approve batcher + const setBatcherOperator = await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setBatcherOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const transferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + + // Alice tries to perform batch transfer on behalf of Bob (should fail) + await expect( + transferBatcher.connect(signers.alice).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, + transferData, + { value: batchTransferFee } + ) + ) + .to.be.revertedWithCustomError(transferBatcher, "ERC7984UnauthorizedSpender") + .withArgs(signers.bob.address, signers.alice.address); + }); + + it("should allow operator to retry transactions on behalf of token holder", async function () { + // Mint tokens to Bob + const mint = await cErc20.mint(signers.bob, 100_000_000); + await mint.wait(); + + // Bob approves Alice as operator + const setOperator = await cErc20.connect(signers.bob).setOperator(signers.alice.address, Math.floor(Date.now() / 1000) + 6000); + await setOperator.wait(); + + const setBatcherOperator = await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await setBatcherOperator.wait(); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + // Original transfer by Alice (as Bob's operator) + const originalTransferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const originalTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + const originalBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, + originalTransferData, + { value: batchTransferFee } + ); + await originalBatchTransfer.wait(); + + // Verify original transaction was recorded with Bob as sender + expect(await transferBatcher.txIdToSender(cErc20Address, originalTxId)).to.equal(signers.bob.address); + + // Retry the same transfer (Alice acting as Bob's operator) + const encryptedRetryAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .encrypt(); + + const retryTransferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedRetryAmount.handles[0], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(originalTxId), + } + ]; + + const retryTxId = await cErc20.nextTxId(); + const retryBatchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, + retryTransferData, + { value: batchTransferFee } + ); + const retryReceipt = await retryBatchTransfer.wait(); + + // Check retry event was emitted with Bob as sender + const retryEvents = getRetryTransferEvent(retryReceipt); + expect(retryEvents.length).to.equal(1); + const retryEvent = retryEvents[0]; + expect(retryEvent.args[0]).to.equal(cErc20Address); + expect(retryEvent.args[1]).to.equal(signers.bob.address); // Bob is the sender + expect(retryEvent.args[2]).to.equal(originalTxId); + expect(retryEvent.args[3]).to.equal(retryTxId); + + // Verify both transfers went through (charlie should have double the amount) + expect(await getConfidentialBalance(cErc20, signers.charlie)).to.equal(transferAmount * 2); + }); + + it("should reject retry when operator tries to retry another user's transaction", async function () { + // Mint tokens to both Bob and Alice + await cErc20.mint(signers.bob, 100_000_000); + await cErc20.mint(signers.alice, 100_000_000); + + // Bob approves batcher + await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + + // Alice approves batcher and Charlie as operator + await cErc20.connect(signers.alice).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + await cErc20.connect(signers.alice).setOperator(signers.charlie.address, Math.floor(Date.now() / 1000) + 6000); + + const transferAmount = 1_000; + + // Bob performs an original transfer + const encryptedBobAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.bob.address) + .add64(transferAmount) + .encrypt(); + + const bobTransferData = [ + { + "to": signers.delta.address, + "encryptedAmount": encryptedBobAmount.handles[0], + "inputProof": encryptedBobAmount.inputProof, + "retryFor": 0, + } + ]; + + const bobTxId = await cErc20.nextTxId(); + const batchTransferFee = await getBatchTransferFee(adminProvider); + await transferBatcher.connect(signers.bob).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, + bobTransferData, + { value: batchTransferFee } + ); + + // Charlie (Alice's operator) tries to retry Bob's transaction (should fail) + const encryptedRetryAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.charlie.address) + .add64(transferAmount) + .encrypt(); + + const retryTransferData = [ + { + "to": signers.delta.address, + "encryptedAmount": encryptedRetryAmount.handles[0], + "inputProof": encryptedRetryAmount.inputProof, + "retryFor": Number(bobTxId), + } + ]; + + await expect( + transferBatcher.connect(signers.charlie).confidentialBatchTransfer( + cErc20Address, + signers.alice.address, // Charlie is trying to retry using Alice's tokens + retryTransferData, + { value: batchTransferFee } + ) + ).to.be.revertedWithCustomError(transferBatcher, "OnlyOriginalSenderCanRetry"); + }); + + it("should allow operator to perform batch transfer with multiple recipients", async function () { + // Mint tokens to Bob + const mint = await cErc20.mint(signers.bob, 100_000_000); + await mint.wait(); + + const bobBalanceBefore = await getConfidentialBalance(cErc20, signers.bob); + + // Bob approves Alice as operator + await cErc20.connect(signers.bob).setOperator(signers.alice.address, Math.floor(Date.now() / 1000) + 6000); + await cErc20.connect(signers.bob).setOperator(transferBatcherAddress, Math.floor(Date.now() / 1000) + 6000); + + const transferAmount = 1_000; + const encryptedTransferAmount = await fhevm + .createEncryptedInput(transferBatcherAddress, signers.alice.address) + .add64(transferAmount) + .add64(transferAmount * 2) + .add64(transferAmount * 3) + .encrypt(); + + const transferData = [ + { + "to": signers.charlie.address, + "encryptedAmount": encryptedTransferAmount.handles[0], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.delta.address, + "encryptedAmount": encryptedTransferAmount.handles[1], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + }, + { + "to": signers.alice.address, // Alice can receive tokens while being the operator + "encryptedAmount": encryptedTransferAmount.handles[2], + "inputProof": encryptedTransferAmount.inputProof, + "retryFor": 0, + } + ]; + + const batchTransferFee = await getBatchTransferFee(adminProvider); + const batchTransfer = await transferBatcher.connect(signers.alice).confidentialBatchTransfer( + cErc20Address, + signers.bob.address, + transferData, + { value: batchTransferFee } + ); + await batchTransfer.wait(); + + // Verify all transfers went through + const totalTransferred = BigInt(transferAmount * 6); + expect(await getConfidentialBalance(cErc20, signers.bob)).to.equal(bobBalanceBefore - totalTransferred); + expect(await getConfidentialBalance(cErc20, signers.charlie)).to.equal(transferAmount); + expect(await getConfidentialBalance(cErc20, signers.delta)).to.equal(transferAmount * 2); + expect(await getConfidentialBalance(cErc20, signers.alice)).to.equal(transferAmount * 3); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Wrapper.reentrancy.ts b/protocol-contracts/confidential-wrapper/test/Wrapper.reentrancy.ts new file mode 100644 index 0000000000..a54a133c84 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Wrapper.reentrancy.ts @@ -0,0 +1,114 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + deployConfidentialToken, + getWrappedEvent, +} from "./utils"; +import { getSigners, Signers } from "./signers"; +import type { + DeploymentCoordinator, + ERC20WithCallback, + ReentrancyAttacker, + WrapperUpgradeable, + TestERC20, +} from "../types"; +import { deployWrapperFixture } from "./fixtures"; + +async function deployERC20WithCallback(name: string, symbol: string, decimals: number) { + const ERC20WithCallbackFactory = await ethers.getContractFactory("ERC20WithCallback"); + const token = await ERC20WithCallbackFactory.deploy(name, symbol, decimals); + await token.waitForDeployment(); + return token; +} + +async function deployReentrancyAttacker( + wrapper: WrapperUpgradeable, + token: ERC20WithCallback +) { + const AttackerFactory = await ethers.getContractFactory("ReentrancyAttacker"); + const attacker = await AttackerFactory.deploy(await wrapper.getAddress(), await token.getAddress()); + await attacker.waitForDeployment(); + return attacker; +} + +async function deployFixture(signers: Signers) { + const { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory } = + await deployWrapperFixture(signers); + + // Deploy ERC20 token with callback functionality + const callbackToken = await deployERC20WithCallback("CallbackToken", "CBT", 6); + + return { + coordinator, + coordinatorAddress, + adminProvider, + confidentialTokenFactory, + wrapperFactory, + callbackToken, + }; +} + +describe("Wrapper Reentrancy Protection", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let callbackToken: ERC20WithCallback; + let attacker: ReentrancyAttacker; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + // Deploy fixture + ({ coordinator, callbackToken } = await deployFixture(signers)); + + // Fund alice with callback tokens + const mintAmount = ethers.parseUnits("10000", 6); + await callbackToken.mint(signers.alice.address, mintAmount); + + // Deploy confidential token for callback token + const { wrapper } = await deployConfidentialToken(coordinator, callbackToken as TestERC20, signers.alice); + + // Deploy attacker contract + attacker = await deployReentrancyAttacker(wrapper, callbackToken); + const attackerAddress = await attacker.getAddress(); + + // Fund attacker with tokens + await callbackToken.mint(attackerAddress, mintAmount); + }); + + describe("Reentrancy Attack Prevention", function () { + it("should prevent reentrancy attack during wrap()", async function () { + const wrapAmount = ethers.parseUnits("1000", 6); + const maxAttacks = 3; // Try to reenter multiple times + + // Execute attack - the attacker will try to reenter but should be blocked + const attackTx = await attacker.connect(signers.alice).attack(wrapAmount, maxAttacks); + const attackReceipt = await attackTx.wait(); + + // Check if reentrancy was attempted and failed + const reentrancyEvents = attackReceipt?.logs + .filter((log) => { + try { + const parsed = attacker.interface.parseLog({ topics: log.topics, data: log.data }); + return parsed?.name === "ReentrancyAttempted"; + } catch { + return false; + } + }) + .map((log) => attacker.interface.parseLog({ topics: log.topics, data: log.data })); + + // Should have attempted reentrancy + expect(reentrancyEvents?.length).to.be.greaterThan(0, "Should have attempted reentrancy"); + + // All reentrancy attempts should have failed + reentrancyEvents?.forEach((event) => { + expect(event?.args.success).to.equal(false, "Reentrancy should be blocked"); + }); + + // Verify only ONE wrap succeeded + const wrappedEvents = getWrappedEvent(attackReceipt); + expect(wrappedEvents.length).to.equal(1, "Should have exactly 1 successful wrap"); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Wrapper.refund.ts b/protocol-contracts/confidential-wrapper/test/Wrapper.refund.ts new file mode 100644 index 0000000000..c85eeee19e --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Wrapper.refund.ts @@ -0,0 +1,317 @@ +import { ethers, fhevm } from "hardhat"; +import { expect } from "chai"; +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { + deployConfidentialToken, + wrapERC20, + getConfidentialBalance, + getUnwrapFee, + getUnwrapStartedEvent, + getUnwrapFinalizedEvent, +} from "./utils"; +import { getSigners, Signers } from "./signers"; +import type { TestERC20, AdminProvider, DeploymentCoordinator, FeeManager, ERC20FailOnAddressMock } from "../types"; +import { deployWrapperFixture, deployTestERC20Fixture, deployTestUnsafeERC20Fixture } from "./fixtures"; + +async function deployFixture(signers: Signers) { + const { coordinator, adminProvider } = await deployWrapperFixture(signers); + + const usdc = await deployTestERC20Fixture("USDC", 6); + await usdc.mint(signers.alice, ethers.parseUnits("100000", 6)); + + // Deploy a mock that can be configured to fail transfers + const rejectMock = await deployTestUnsafeERC20Fixture("ERC20FailOnAddressMock", "REJECT", 6); + + return { coordinator, adminProvider, usdc, rejectMock }; +} + +describe("Wrapper Refund Address Tests", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let adminProvider: AdminProvider; + let usdc: TestERC20; + let rejectMock: ERC20FailOnAddressMock; + let feeManager: FeeManager; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + ({ coordinator, adminProvider, usdc, rejectMock } = await deployFixture(signers)); + + const feeManagerAddress = await adminProvider.feeManager(); + feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + }); + + describe("Unwrap with different to/refund addresses - Transfer fails", function () { + it("should mint refund to `refund` address when `to` rejects tokens", async function () { + // Use rejectMock as the underlying token so we can control transfer failures + await rejectMock.mint(signers.alice, ethers.parseUnits("100000", 6)); + const { cToken, wrapper } = await deployConfidentialToken(coordinator, rejectMock, signers.alice); + + // Wrap tokens for alice using rejectMock + const wrapAmount = 100_000n; + await wrapERC20(coordinator, rejectMock, wrapAmount, signers.alice.address, signers.alice); + + const aliceCTokenBalanceBefore = await getConfidentialBalance(cToken, signers.alice); + // Bob starts with 0 balance (no handle yet) + const bobCTokenBalanceBefore = 0n; + const rate = await cToken.rate(); + + // Configure rejectMock to reject transfers to itself + const rejectAddress = await rejectMock.getAddress(); + await rejectMock.setFailOnTransferTo(rejectAddress, true); + + // Unwrap: to = rejectMock (will fail), refund = bob (should receive cTokens) + const unwrapAmount = 50_000n; + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signers.alice.address) + .add64(unwrapAmount) + .encrypt(); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [rejectAddress, signers.bob.address, "0x"] // to=reject, refund=bob + ); + + const unwrapTx = await cToken + .connect(signers.alice) + ["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Verify event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + expect(unwrapStartedEvent.args.to).to.equal(rejectAddress); + expect(unwrapStartedEvent.args.refund).to.equal(signers.bob.address); + + // Finalize unwrap + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof + ); + const finalizeReceipt = await unwrapFinalizedTx.wait(); + + // Verify UnwrappedFinalized event shows failure + const finalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(finalizedEvents.length).to.equal(1); + expect(finalizedEvents[0].args.finalizeSuccess).to.equal(false); // Transfer failed + expect(finalizedEvents[0].args.feeTransferSuccess).to.equal(true); // Fee succeeded + + // Verify balances + const aliceCTokenBalanceAfter = await getConfidentialBalance(cToken, signers.alice); + const bobCTokenBalanceAfter = await getConfidentialBalance(cToken, signers.bob); + const rejectBalance = await rejectMock.balanceOf(rejectAddress); + + const unwrapFee = await getUnwrapFee(wrapper, unwrapAmount); + + // Alice lost unwrapAmount + expect(aliceCTokenBalanceBefore - aliceCTokenBalanceAfter).to.equal(unwrapAmount); + + // Bob received (unwrapAmount - fee) back as cTokens + expect(bobCTokenBalanceAfter - bobCTokenBalanceBefore).to.equal(unwrapAmount - unwrapFee); + + // rejectMock received nothing + expect(rejectBalance).to.equal(0n); + }); + + it("should mint full amount to `refund` when both transfers fail", async function () { + // Use rejectMock as the underlying token so we can control transfer failures + await rejectMock.mint(signers.alice, ethers.parseUnits("100000", 6)); + const { cToken, wrapper } = await deployConfidentialToken(coordinator, rejectMock, signers.alice); + + // Wrap tokens using rejectMock + const wrapAmount = 100_000n; + await wrapERC20(coordinator, rejectMock, wrapAmount, signers.alice.address, signers.alice); + + const aliceCTokenBalanceBefore = await getConfidentialBalance(cToken, signers.alice); + // Bob starts with 0 balance (no handle yet) + const bobCTokenBalanceBefore = 0n; + + // Configure both recipient and fee recipient to reject + const rejectAddress = await rejectMock.getAddress(); + await rejectMock.setFailOnTransferTo(rejectAddress, true); + await rejectMock.setFailOnTransferTo(signers.royalties.address, true); + + // Unwrap: to = rejectMock (will fail), refund = bob + const unwrapAmount = 50_000n; + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signers.alice.address) + .add64(unwrapAmount) + .encrypt(); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [rejectAddress, signers.bob.address, "0x"] + ); + + const unwrapTx = await cToken + .connect(signers.alice) + ["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Finalize + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof + ); + const finalizeReceipt = await unwrapFinalizedTx.wait(); + + // Verify event shows both transfers failed + const finalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(finalizedEvents[0].args.finalizeSuccess).to.equal(false); + expect(finalizedEvents[0].args.feeTransferSuccess).to.equal(false); + + // Verify bob received FULL amount back (no fees deducted) + const aliceCTokenBalanceAfter = await getConfidentialBalance(cToken, signers.alice); + const bobCTokenBalanceAfter = await getConfidentialBalance(cToken, signers.bob); + + expect(aliceCTokenBalanceBefore - aliceCTokenBalanceAfter).to.equal(unwrapAmount); + expect(bobCTokenBalanceAfter - bobCTokenBalanceBefore).to.equal(unwrapAmount); // Full amount + }); + }); + + describe("Zero address validation", function () { + it("should revert when `to` is address(0)", async function () { + const { cToken, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Wrap tokens + const wrapAmount = 100_000n; + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const unwrapAmount = 50_000n; + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signers.alice.address) + .add64(unwrapAmount) + .encrypt(); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [ethers.ZeroAddress, signers.alice.address, "0x"] // to = zero address + ); + + await expect( + cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data + ) + ).to.be.revertedWithCustomError(wrapper, "CannotSendToZeroAddress"); + }); + + it("should revert when `refund` is address(0)", async function () { + const { cToken, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Wrap tokens + const wrapAmount = 100_000n; + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const unwrapAmount = 50_000n; + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signers.alice.address) + .add64(unwrapAmount) + .encrypt(); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, ethers.ZeroAddress, "0x"] // refund = zero address + ); + + await expect( + cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data + ) + ).to.be.revertedWithCustomError(wrapper, "CannotSendToZeroAddress"); + }); + }); + + describe("Failed unwrap path (actualBurnAmount mismatch)", function () { + it("should mint to refund address when actualBurnAmount != expectedBurnAmount", async function () { + const { cToken, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Wrap a small amount for alice + const wrapAmount = 100n; + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCTokenBalanceBefore = await getConfidentialBalance(cToken, signers.alice); + + // Try to unwrap MORE than alice has - this will cause actualBurnAmount < expectedBurnAmount + const unwrapAmount = aliceCTokenBalanceBefore + 100n; + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signers.alice.address) + .add64(unwrapAmount) + .encrypt(); + + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.bob.address, "0x"] // refund = bob + ); + + const unwrapTx = await cToken + .connect(signers.alice) + ["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Finalize + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof + ); + await unwrapFinalizedTx.wait(); + + // Verify: alice balance should remain the same (nothing burned) + const aliceCTokenBalanceAfter = await getConfidentialBalance(cToken, signers.alice); + expect(aliceCTokenBalanceAfter).to.equal(aliceCTokenBalanceBefore); + + // Bob should receive the refund, but the transfer is skipped since the actualBurnAmount was 0 + expect(await cToken.confidentialBalanceOf(signers.bob)).to.equal( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Wrapper.ts b/protocol-contracts/confidential-wrapper/test/Wrapper.ts new file mode 100644 index 0000000000..52081c432f --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Wrapper.ts @@ -0,0 +1,1070 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + getWrapDeployedEvent, + deployConfidentialToken, + deployConfidentialETH, + getDeployConfidentialTokenTx, + getWrapFeeBasisPoints +} from "./utils"; +import { getSigners, Signers } from "./signers"; + +import type { TestERC20, AdminProvider, DeploymentCoordinator, WrapperFactory, RegulatedERC7984UpgradeableFactory } from "../types"; +import { MAX_DECIMALS } from "./constants"; +import { deployAdminProviderFixture, deployConfidentialTokenFactoryFixture, deployERC20InvalidDecimalsFixture, deployERC20NoDecimalsFixture, deployTestERC20Fixture, deployWrapperFactoryFixture, deployWrapperFixture } from "./fixtures"; + + +async function deployFixture(signers: Signers) { + const { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory } = await deployWrapperFixture(signers); + + const usdc = await deployTestERC20Fixture("USDC"); + const usdcAddress = await usdc.getAddress(); + + let transaction = await usdc.mint(signers.alice, 1000); + await transaction.wait(); + + const usdt = await deployTestERC20Fixture("USDT"); + const usdtAddress = await usdt.getAddress(); + + transaction = await usdt.mint(signers.alice, 1000); + await transaction.wait(); + + return { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory, usdc, usdcAddress, usdt, usdtAddress }; +} + +describe("Wrapper", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let coordinatorAddress: string; + let adminProvider: AdminProvider; + let usdc: TestERC20; + let usdcAddress: string; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + ({ + coordinator, + coordinatorAddress, + adminProvider, + usdc, + usdcAddress, + } = await deployFixture(signers)); + }); + + describe("Deploy", function () { + it("should configure the right permissions", async function () { + await deployConfidentialToken(coordinator, usdc, signers.alice); + + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + const cUsdc = await ethers.getContractAt("RegulatedERC7984Upgradeable", cUsdcAddress); + const wrapperAddress = await coordinator.getWrapper(usdcAddress); + const wrapper = await ethers.getContractAt("Wrapper", wrapperAddress); + + expect(await cUsdc.hasRole(await cUsdc.DEFAULT_ADMIN_ROLE(), wrapperAddress)).to.equal(false); + expect(await cUsdc.hasRole(await cUsdc.WRAPPER_ROLE(), wrapperAddress)).to.equal(true); + expect(await cUsdc.hasRole(await cUsdc.DEFAULT_ADMIN_ROLE(), await adminProvider.owner())).to.equal(true); + expect(await cUsdc.hasRole(await cUsdc.DEFAULT_ADMIN_ROLE(), coordinatorAddress)).to.equal(false); + }); + + it("should not deploy new encrypted erc20 when paying less than required fee", async function () { + // Get deploy fee from FeeManager + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + const insufficientFee = deployTokenFee > 0n ? deployTokenFee - 1n : 0n; + const tx = coordinator.deploy(usdcAddress, { + value: insufficientFee, + }); + await expect(tx).to.be.revertedWithCustomError(coordinator, "IncorrectDeployFee"); + }); + + it("should not deploy new encrypted erc20 when paying more than required fee", async function () { + // Get deploy fee from FeeManager + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + const excessiveFee = deployTokenFee + 1n; + const tx = coordinator.deploy(usdcAddress, { + value: excessiveFee, + }); + await expect(tx).to.be.revertedWithCustomError(coordinator, "IncorrectDeployFee"); + }); + + it("should deploy new encrypted erc20 when paying exact fee", async function () { + // Get deploy fee from FeeManager + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + // Deploy with exact fee should succeed + await expect( + coordinator.deploy(usdcAddress, { value: deployTokenFee }) + ).to.not.be.reverted; + + // Verify deployment was successful + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + expect(cUsdcAddress).to.not.equal(ethers.ZeroAddress); + + const wrapperAddress = await coordinator.getWrapper(usdcAddress); + expect(wrapperAddress).to.not.equal(ethers.ZeroAddress); + }); + + it("should deploy new encrypted ETH", async function () { + // Get deploy fee from FeeManager + const { cEth, cEthAddress, wrapperAddress, wrapper, receipt } = await deployConfidentialETH(coordinator, signers.alice); + + expect(await cEth.name()).to.equal("confidential Ethereum"); + expect(await cEth.symbol()).to.equal("cETH"); + expect(await cEth.decimals()).to.equal(MAX_DECIMALS); + + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent.length).to.be.equal(1); + expect(wrapDeployedEvent[0].args[0]).to.be.equal(ethers.ZeroAddress); + expect(wrapDeployedEvent[0].args[1]).to.be.equal(wrapperAddress); + expect(wrapDeployedEvent[0].args[2]).to.be.equal(cEthAddress); + expect(wrapDeployedEvent[0].args[3]).to.be.equal("Ethereum"); + expect(wrapDeployedEvent[0].args[4]).to.be.equal("ETH"); + expect(wrapDeployedEvent[0].args[5]).to.be.equal(18); + expect(wrapDeployedEvent[0].args[6]).to.be.equal(signers.alice); + }); + + for (const originalDecimals of [5, 6, 18]) { + let itName = "equal to"; + if (originalDecimals > MAX_DECIMALS) { + itName = "greater than"; + } else if (originalDecimals < MAX_DECIMALS) { + itName = "lesser than"; + } + it(`should deploy new encrypted erc20 with from erc20 with decimals ${itName} MAX_DECIMALS`, async function () { + const originalName = "My Token"; + const originalSymbol = "TOK"; + const erc20 = await deployTestERC20Fixture(originalSymbol, originalDecimals, originalName); + const erc20Address = await erc20.getAddress(); + + const { receipt } = await deployConfidentialToken(coordinator, erc20, signers.alice); + + const cErc20Address = await coordinator.getConfidentialToken(erc20Address); + const cErc20 = await ethers.getContractAt("RegulatedERC7984Upgradeable", cErc20Address); + const wrapperAddress = await coordinator.getWrapper(erc20Address); + const wrapper = await ethers.getContractAt("Wrapper", wrapperAddress); + + expect(await cErc20.name()).to.equal(`confidential ${originalName}`); + expect(await cErc20.symbol()).to.equal(`c${originalSymbol}`); + if (originalDecimals > MAX_DECIMALS) { + expect(await cErc20.decimals()).to.equal(MAX_DECIMALS); + } else { + expect(await cErc20.decimals()).to.equal(originalDecimals); + } + + expect(await cErc20.hasRole(await cErc20.DEFAULT_ADMIN_ROLE(), wrapperAddress)).to.equal(false); + expect(await cErc20.hasRole(await cErc20.WRAPPER_ROLE(), wrapperAddress)).to.equal(true); + expect(await cErc20.regulator()).to.equal(await adminProvider.regulator()); + + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent.length).to.be.equal(1); + expect(wrapDeployedEvent[0].args[0]).to.be.equal(erc20Address); + expect(wrapDeployedEvent[0].args[1]).to.be.equal(wrapperAddress); + expect(wrapDeployedEvent[0].args[2]).to.be.equal(cErc20Address); + expect(wrapDeployedEvent[0].args[3]).to.be.equal(originalName); + expect(wrapDeployedEvent[0].args[4]).to.be.equal(originalSymbol); + expect(wrapDeployedEvent[0].args[5]).to.be.equal(originalDecimals); + expect(wrapDeployedEvent[0].args[6]).to.be.equal(signers.alice); + }); + } + + it("should revert if encrypted erc20 already exists", async function () { + await deployConfidentialToken(coordinator, usdc, signers.alice); + + await expect(getDeployConfidentialTokenTx(coordinator, usdc, signers.alice)).to.be.revertedWithCustomError( + coordinator, + "WrapperAlreadyExists", + ); + }); + + it("should deploy tokens with admin provider from wrapper", async function () { + // Deploy a confidential token + await deployConfidentialToken(coordinator, usdc, signers.alice); + + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + const cUsdc = await ethers.getContractAt("RegulatedERC7984Upgradeable", cUsdcAddress); + + // Verify the token has the same admin provider as the coordinator + expect(await cUsdc.adminProvider()).to.equal(await coordinator.adminProvider()); + }); + + it("should revert on fee transfer failure", async function () { + // Deploy a contract that rejects ETH transfers + const RejectEthContract = await ethers.getContractFactory("RejectEth"); + const rejectEth = await RejectEthContract.deploy(); + await rejectEth.waitForDeployment(); + + // Update fee manager to use the rejecting contract as fee recipient + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + await feeManager.setFeeRecipient(await rejectEth.getAddress()); + + // Get deploy fee + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + // Attempt to deploy should fail due to fee transfer failure + await expect( + coordinator.deploy(usdcAddress, { value: deployTokenFee }) + ).to.be.revertedWithCustomError(coordinator, "FeeTransferFailed"); + }); + + describe("Protection against griefing attacks", function () { + it("should prevent deploying wrapper for non-existent token (no code at address)", async function () { + // Attacker tries to front-run a CREATE2 token deployment + // by deploying wrapper for an address with no code + const nonExistentTokenAddress = "0x000000000000000000000004500000000000007B"; + + // Get deploy fee + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + // Attempt to deploy should fail because token doesn't exist + await expect( + coordinator.connect(signers.charlie).deploy(nonExistentTokenAddress, { value: deployTokenFee }) + ).to.be.revertedWithCustomError(coordinator, "TokenMustExist"); + + // Verify no wrapper or cToken was deployed + expect(await coordinator.getWrapper(nonExistentTokenAddress)).to.equal(ethers.ZeroAddress); + expect(await coordinator.getConfidentialToken(nonExistentTokenAddress)).to.equal(ethers.ZeroAddress); + expect(await coordinator.wrapperExists(nonExistentTokenAddress)).to.equal(false); + }); + + it("should prevent deployment for EOA (externally owned account)", async function () { + // EOAs have no code, so should be rejected + const eoaAddress = ethers.Wallet.createRandom().address; + + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + await expect( + coordinator.deploy(eoaAddress, { value: deployTokenFee }) + ).to.be.revertedWithCustomError(coordinator, "TokenMustExist"); + }); + }); + }); + + describe("Constructor Validation", function () { + it("should revert if admin provider is zero address", async function () { + const WrapperFactoryContract = await ethers.getContractFactory("WrapperFactory"); + const RegulatedERC7984UpgradeableFactoryContract = await ethers.getContractFactory("RegulatedERC7984UpgradeableFactory"); + const DeploymentCoordinatorContract = await ethers.getContractFactory("DeploymentCoordinator"); + + const wrapperFactory = await WrapperFactoryContract.deploy(); + const confidentialTokenFactory = await RegulatedERC7984UpgradeableFactoryContract.deploy(); + + await expect( + DeploymentCoordinatorContract.deploy( + ethers.ZeroAddress, // adminProvider + wrapperFactory, + confidentialTokenFactory + ) + ).to.be.revertedWithCustomError(DeploymentCoordinatorContract, "ZeroAddressAdminProvider"); + }); + + it("should revert if wrapper factory is zero address", async function () { + const RegulatedERC7984UpgradeableFactoryContract = await ethers.getContractFactory("RegulatedERC7984UpgradeableFactory"); + const DeploymentCoordinatorContract = await ethers.getContractFactory("DeploymentCoordinator"); + + const confidentialTokenFactory = await RegulatedERC7984UpgradeableFactoryContract.deploy(); + + await expect( + DeploymentCoordinatorContract.deploy( + adminProvider, + ethers.ZeroAddress, // wrapperFactory + confidentialTokenFactory + ) + ).to.be.revertedWithCustomError(DeploymentCoordinatorContract, "ZeroAddressWrapperFactory"); + }); + + it("should revert if confidential token factory is zero address", async function () { + const WrapperFactoryContract = await ethers.getContractFactory("WrapperFactory"); + const DeploymentCoordinatorContract = await ethers.getContractFactory("DeploymentCoordinator"); + + const wrapperFactory = await WrapperFactoryContract.deploy(); + + await expect( + DeploymentCoordinatorContract.deploy( + adminProvider, + wrapperFactory, + ethers.ZeroAddress // confidentialTokenFactory + ) + ).to.be.revertedWithCustomError(DeploymentCoordinatorContract, "ZeroAddressConfidentialTokenFactory"); + }); + }); + + describe("Fees", function () { + it("should return wrap fee", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const amount = BigInt(100); + + const wrapFeeBasisPoints = await getWrapFeeBasisPoints(wrapper); + const adminProviderAddress = await wrapper.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFee = await feeManager.getWrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + const expectedWrapPrice = (amount * wrapFeeBasisPoints) / BigInt(10_000); + + expect(wrapFee).to.equal(expectedWrapPrice); + }); + }); + + describe("DeploymentCoordinator wrapperExists", function () { + it("should return false for non-deployed wrapper", async function () { + expect(await coordinator.wrapperExists(usdcAddress)).to.equal(false); + }); + + it("should return true for deployed ERC20 wrapper", async function () { + await deployConfidentialToken(coordinator, usdc, signers.alice); + expect(await coordinator.wrapperExists(usdcAddress)).to.equal(true); + }); + + it("should return true for deployed ETH wrapper", async function () { + await deployConfidentialETH(coordinator, signers.alice); + expect(await coordinator.wrapperExists(ethers.ZeroAddress)).to.equal(true); + }); + + it("should return false for random address", async function () { + const randomAddress = ethers.Wallet.createRandom().address; + expect(await coordinator.wrapperExists(randomAddress)).to.equal(false); + }); + }); + + describe("DeploymentCoordinator setters", function () { + it("should allow owner to set adminProvider and emit event", async function () { + const { adminProviderAddress: newAdminProviderAddress } = await deployAdminProviderFixture(signers); + + const oldAdminProvider = await coordinator.adminProvider(); + const tx = await coordinator.setAdminProvider(newAdminProviderAddress); + + await expect(tx) + .to.emit(coordinator, "AdminProviderUpdated") + .withArgs(oldAdminProvider, newAdminProviderAddress); + + expect(await coordinator.adminProvider()).to.equal(newAdminProviderAddress); + }); + + it("should revert when non-owner tries to set adminProvider", async function () { + const { adminProviderAddress: newAdminProviderAddress } = await deployAdminProviderFixture(signers); + + await expect( + coordinator.connect(signers.alice).setAdminProvider(newAdminProviderAddress) + ).to.be.revertedWithCustomError(coordinator, "OwnableUnauthorizedAccount"); + }); + + it("should revert when setting adminProvider to zero address", async function () { + await expect( + coordinator.setAdminProvider(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(coordinator, "ZeroAddressAdminProvider"); + }); + + it("should allow owner to set wrapperFactory and emit event", async function () { + const { wrapperFactoryAddress: newWrapperFactoryAddress } = await deployWrapperFactoryFixture(); + + const oldWrapperFactory = await coordinator.wrapperFactory(); + const tx = await coordinator.setWrapperFactory(newWrapperFactoryAddress); + + await expect(tx) + .to.emit(coordinator, "WrapperFactoryUpdated") + .withArgs(oldWrapperFactory, newWrapperFactoryAddress); + + expect(await coordinator.wrapperFactory()).to.equal(newWrapperFactoryAddress); + }); + + it("should revert when non-owner tries to set wrapperFactory", async function () { + const { wrapperFactoryAddress: newWrapperFactoryAddress } = await deployWrapperFactoryFixture(); + + await expect( + coordinator.connect(signers.alice).setWrapperFactory(newWrapperFactoryAddress) + ).to.be.revertedWithCustomError(coordinator, "OwnableUnauthorizedAccount"); + }); + + it("should revert when setting wrapperFactory to zero address", async function () { + await expect( + coordinator.setWrapperFactory(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(coordinator, "ZeroAddressWrapperFactory"); + }); + + it("should allow owner to set confidentialTokenFactory and emit event", async function () { + const { confidentialTokenFactoryAddress: newConfidentialTokenFactoryAddress } = await deployConfidentialTokenFactoryFixture(); + + const oldConfidentialTokenFactory = await coordinator.confidentialTokenFactory(); + const tx = await coordinator.setConfidentialTokenFactory(newConfidentialTokenFactoryAddress); + + await expect(tx) + .to.emit(coordinator, "ConfidentialTokenFactoryUpdated") + .withArgs(oldConfidentialTokenFactory, newConfidentialTokenFactoryAddress); + + expect(await coordinator.confidentialTokenFactory()).to.equal(newConfidentialTokenFactoryAddress); + }); + + it("should revert when non-owner tries to set confidentialTokenFactory", async function () { + const { confidentialTokenFactoryAddress: newConfidentialTokenFactoryAddress } = await deployConfidentialTokenFactoryFixture(); + + await expect( + coordinator.connect(signers.alice).setConfidentialTokenFactory(newConfidentialTokenFactoryAddress) + ).to.be.revertedWithCustomError(coordinator, "OwnableUnauthorizedAccount"); + }); + + it("should revert when setting confidentialTokenFactory to zero address", async function () { + await expect( + coordinator.setConfidentialTokenFactory(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(coordinator, "ZeroAddressConfidentialTokenFactory"); + }); + }); + + describe("deploy with _tryGetAssetDecimals fallback", function () { + it("should use fallback decimals (18) when token has no decimals function", async function () { + const { tokenNoDecimals } = await deployERC20NoDecimalsFixture(); + const { cToken } = await deployConfidentialToken(coordinator, tokenNoDecimals, signers.alice); + + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); + expect(await cToken.rate()).to.equal(BigInt(10) ** BigInt(12)); + }); + + it("should use fallback decimals (18) when token returns invalid decimals data", async function () { + const { tokenInvalidDecimals } = await deployERC20InvalidDecimalsFixture(); + const { cToken } = await deployConfidentialToken(coordinator, tokenInvalidDecimals, signers.alice); + + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); + expect(await cToken.rate()).to.equal(BigInt(10) ** BigInt(12)); + }); + }); + + describe("setWrapperImplementation", function () { + it("should deploy new wrappers with updated implementation after calling setWrapperImplementation", async function () { + // Deploy first wrapper with default implementation + await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapperAddress = await coordinator.getWrapper(usdcAddress); + + // Get implementation of first wrapper + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const impl1 = await ethers.provider.getStorage(wrapperAddress, implementationSlot); + const impl1Address = "0x" + impl1.slice(26); + + // Deploy V2 implementation + const WrapperV2 = await ethers.getContractFactory("WrapperUpgradeableV2Mock"); + const v2Impl = await WrapperV2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Update coordinator's canonical implementation to V2 + await coordinator.setWrapperImplementation(v2ImplAddress); + + // Verify coordinator now points to V2 implementation + expect(await coordinator.wrapperImplementation()).to.equal(v2ImplAddress); + + // Deploy second wrapper (USDT) - should use V2 implementation + const usdt = await deployTestERC20Fixture("USDT"); + const usdtAddress = await usdt.getAddress(); + await deployConfidentialToken(coordinator, usdt, signers.alice); + const wrapper2Address = await coordinator.getWrapper(usdtAddress); + + // Get implementation of second wrapper + const impl2 = await ethers.provider.getStorage(wrapper2Address, implementationSlot); + const impl2Address = "0x" + impl2.slice(26); + + // Verify second wrapper uses V2 implementation + expect(impl2Address.toLowerCase()).to.equal(v2ImplAddress.toLowerCase()); + + // Verify first wrapper still uses V1 implementation (unchanged) + const impl1After = await ethers.provider.getStorage(wrapperAddress, implementationSlot); + const impl1AfterAddress = "0x" + impl1After.slice(26); + expect(impl1AfterAddress.toLowerCase()).to.equal(impl1Address.toLowerCase()); + expect(impl1AfterAddress.toLowerCase()).to.not.equal(v2ImplAddress.toLowerCase()); + + // Verify V2 functionality works on second wrapper + const wrapper2V2 = await ethers.getContractAt("WrapperUpgradeableV2Mock", wrapper2Address); + expect(await wrapper2V2.counter()).to.equal(0); + await wrapper2V2.incrementCounter(); + expect(await wrapper2V2.counter()).to.equal(1); + + // Verify first wrapper does NOT have V2 functionality (still V1) + const wrapper1AsV2 = await ethers.getContractAt("WrapperUpgradeableV2Mock", wrapperAddress); + await expect(wrapper1AsV2.counter()).to.be.reverted; + }); + + it("should prevent non-owner from calling setWrapperImplementation", async function () { + const WrapperV2 = await ethers.getContractFactory("WrapperUpgradeableV2Mock"); + const v2Impl = await WrapperV2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + await expect( + coordinator.connect(signers.alice).setWrapperImplementation(v2ImplAddress) + ).to.be.revertedWithCustomError(coordinator, "OwnableUnauthorizedAccount") + .withArgs(signers.alice.address); + }); + + it("should revert when setting zero address as implementation", async function () { + await expect( + coordinator.setWrapperImplementation(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(coordinator, "ZeroAddressImplementation"); + }); + }); + + describe("setConfidentialTokenImplementation", function () { + it("should deploy new tokens with updated implementation after calling setConfidentialTokenImplementation", async function () { + // Deploy first token with default implementation + await deployConfidentialToken(coordinator, usdc, signers.alice); + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + + // Get implementation of first token + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const impl1 = await ethers.provider.getStorage(cUsdcAddress, implementationSlot); + const impl1Address = "0x" + impl1.slice(26); + + // Deploy V2 implementation + const RegulatedERC7984V2 = await ethers.getContractFactory("RegulatedERC7984UpgradeableV2Mock"); + const v2Impl = await RegulatedERC7984V2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Update coordinator's canonical implementation to V2 + await coordinator.setConfidentialTokenImplementation(v2ImplAddress); + + // Verify coordinator now points to V2 implementation + expect(await coordinator.confidentialTokenImplementation()).to.equal(v2ImplAddress); + + // Deploy second token (USDT) - should use V2 implementation + const usdt = await deployTestERC20Fixture("USDT"); + const usdtAddress = await usdt.getAddress(); + await deployConfidentialToken(coordinator, usdt, signers.alice); + const cUsdtAddress = await coordinator.getConfidentialToken(usdtAddress); + + // Get implementation of second token + const impl2 = await ethers.provider.getStorage(cUsdtAddress, implementationSlot); + const impl2Address = "0x" + impl2.slice(26); + + // Verify second token uses V2 implementation + expect(impl2Address.toLowerCase()).to.equal(v2ImplAddress.toLowerCase()); + + // Verify first token still uses V1 implementation (unchanged) + const impl1After = await ethers.provider.getStorage(cUsdcAddress, implementationSlot); + const impl1AfterAddress = "0x" + impl1After.slice(26); + expect(impl1AfterAddress.toLowerCase()).to.equal(impl1Address.toLowerCase()); + expect(impl1AfterAddress.toLowerCase()).to.not.equal(v2ImplAddress.toLowerCase()); + + // Verify V2 functionality works on second token + const cUsdtV2 = await ethers.getContractAt("RegulatedERC7984UpgradeableV2Mock", cUsdtAddress); + expect(await cUsdtV2.counter()).to.equal(0); + await cUsdtV2.incrementCounter(); + expect(await cUsdtV2.counter()).to.equal(1); + + // Verify first token does NOT have V2 functionality (still V1) + const cUsdcAsV2 = await ethers.getContractAt("RegulatedERC7984UpgradeableV2Mock", cUsdcAddress); + await expect(cUsdcAsV2.counter()).to.be.reverted; + }); + + it("should prevent non-owner from calling setConfidentialTokenImplementation", async function () { + const RegulatedERC7984V2 = await ethers.getContractFactory("RegulatedERC7984UpgradeableV2Mock"); + const v2Impl = await RegulatedERC7984V2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + await expect( + coordinator.connect(signers.alice).setConfidentialTokenImplementation(v2ImplAddress) + ).to.be.revertedWithCustomError(coordinator, "OwnableUnauthorizedAccount") + .withArgs(signers.alice.address); + }); + + it("should revert when setting zero address as implementation", async function () { + await expect( + coordinator.setConfidentialTokenImplementation(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(coordinator, "ZeroAddressImplementation"); + }); + }); + + describe("Upgrade deployed RegulatedERC7984Upgradeable contract", function () { + it("should allow upgrading with UPGRADER_ROLE", async function () { + await deployConfidentialToken(coordinator, usdc, signers.alice); + + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + const cUsdc = await ethers.getContractAt("RegulatedERC7984Upgradeable", cUsdcAddress); + + // Admin provider owner has DEFAULT_ADMIN_ROLE + const adminProviderOwner = await adminProvider.owner(); + + // Grant UPGRADER_ROLE to bob + await ethers.provider.send("hardhat_impersonateAccount", [adminProviderOwner]); + const adminSigner = await ethers.getSigner(adminProviderOwner); + const UPGRADER_ROLE = await cUsdc.UPGRADER_ROLE(); + await cUsdc.connect(adminSigner).grantRole(UPGRADER_ROLE, signers.bob.address); + await ethers.provider.send("hardhat_stopImpersonatingAccount", [adminProviderOwner]); + + const RegulatedERC7984UpgradeableV2Mock = await ethers.getContractFactory("RegulatedERC7984UpgradeableV2Mock"); + const newImpl = await RegulatedERC7984UpgradeableV2Mock.deploy(); + await newImpl.waitForDeployment(); + + // Bob with UPGRADER_ROLE can upgrade + const tx = await cUsdc.connect(signers.bob).upgradeToAndCall(await newImpl.getAddress(), "0x"); + await tx.wait(); + + const cUsdcV2 = await ethers.getContractAt("RegulatedERC7984UpgradeableV2Mock", cUsdcAddress); + await cUsdcV2.connect(signers.bob).incrementCounter(); + expect(await cUsdcV2.counter()).to.equal(1); + }); + + it("should NOT allow upgrading without UPGRADER_ROLE", async function () { + await deployConfidentialToken(coordinator, usdc, signers.alice); + + const cUsdcAddress = await coordinator.getConfidentialToken(usdcAddress); + const cUsdc = await ethers.getContractAt("RegulatedERC7984Upgradeable", cUsdcAddress); + + const UPGRADER_ROLE = await cUsdc.UPGRADER_ROLE(); + expect(await cUsdc.hasRole(UPGRADER_ROLE, signers.alice.address)).to.equal(false); + + const RegulatedERC7984UpgradeableV2Mock = await ethers.getContractFactory("RegulatedERC7984UpgradeableV2Mock"); + const newImpl = await RegulatedERC7984UpgradeableV2Mock.deploy(); + await newImpl.waitForDeployment(); + + await expect( + cUsdc.connect(signers.alice).upgradeToAndCall(await newImpl.getAddress(), "0x") + ).to.be.revertedWithCustomError( + cUsdc, + "AccessControlUnauthorizedAccount", + ).withArgs(signers.alice.address, UPGRADER_ROLE); + }); + }); + + describe("Non-Standard ERC20 Token Support", function () { + describe("ERC20 with bytes32 name/symbol (like MKR)", function () { + it("should deploy wrapper for token with bytes32 metadata", async function () { + // Deploy ERC20 with bytes32 metadata (like MKR) + const ERC20Bytes32MetadataFactory = await ethers.getContractFactory("ERC20Bytes32Metadata"); + + // Convert strings to bytes32 + const nameBytes32 = ethers.encodeBytes32String("Maker"); + const symbolBytes32 = ethers.encodeBytes32String("MKR"); + + const mkrLikeToken = await ERC20Bytes32MetadataFactory.deploy( + nameBytes32, + symbolBytes32, + 18 + ); + await mkrLikeToken.waitForDeployment(); + const mkrLikeTokenAddress = await mkrLikeToken.getAddress(); + + // Mint some tokens for testing + await mkrLikeToken.mint(signers.alice, 1000); + + // Deploy confidential wrapper + const { cToken, wrapper, receipt } = await deployConfidentialToken(coordinator, mkrLikeToken, signers.alice); + + // Verify deployment succeeded + expect(await coordinator.wrapperExists(mkrLikeTokenAddress)).to.equal(true); + + // Verify confidential token was created with parsed metadata + expect(await cToken.name()).to.equal("confidential Maker"); + expect(await cToken.symbol()).to.equal("cMKR"); + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); + + // Verify event contains correct metadata + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent.length).to.equal(1); + expect(wrapDeployedEvent[0].args[3]).to.equal("Maker"); + expect(wrapDeployedEvent[0].args[4]).to.equal("MKR"); + expect(wrapDeployedEvent[0].args[5]).to.equal(18); + }); + + it("should handle bytes32 metadata with trailing zeros", async function () { + const ERC20Bytes32MetadataFactory = await ethers.getContractFactory("ERC20Bytes32Metadata"); + + // Short name/symbol will have trailing zeros + const nameBytes32 = ethers.encodeBytes32String("Dai"); + const symbolBytes32 = ethers.encodeBytes32String("DAI"); + + const daiLikeToken = await ERC20Bytes32MetadataFactory.deploy( + nameBytes32, + symbolBytes32, + 18 + ); + await daiLikeToken.waitForDeployment(); + + const { cToken } = await deployConfidentialToken(coordinator, daiLikeToken, signers.alice); + + // Verify trailing zeros were properly removed + expect(await cToken.name()).to.equal("confidential Dai"); + expect(await cToken.symbol()).to.equal("cDAI"); + }); + }); + + describe("ERC20 that reverts on name/symbol", function () { + it("should deploy wrapper using address fallback when name/symbol revert", async function () { + const ERC20RevertingMetadataFactory = await ethers.getContractFactory("ERC20RevertingMetadata"); + const revertingToken = await ERC20RevertingMetadataFactory.deploy(18); + await revertingToken.waitForDeployment(); + const revertingTokenAddress = await revertingToken.getAddress(); + + await revertingToken.mint(signers.alice, 1000); + + const { cToken, receipt } = await deployConfidentialToken(coordinator, revertingToken, signers.alice); + + // Verify deployment succeeded + expect(await coordinator.wrapperExists(revertingTokenAddress)).to.equal(true); + + // Verify fallback to address-based naming + const expectedName = ethers.toBeHex(BigInt(revertingTokenAddress), 20); + const expectedSymbol = expectedName.slice(0, 8); // "0x" + 6 chars + + expect(await cToken.name()).to.equal(`confidential ${expectedName}`); + expect(await cToken.symbol()).to.equal(`c${expectedSymbol}`); + + // Verify event uses fallback names + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent[0].args[3]).to.equal(expectedName); + expect(wrapDeployedEvent[0].args[4]).to.equal(expectedSymbol); + }); + }); + + describe("ERC20 with empty name/symbol", function () { + it("should deploy wrapper using address fallback when name/symbol are empty", async function () { + const ERC20EmptyMetadataFactory = await ethers.getContractFactory("ERC20EmptyMetadata"); + const emptyToken = await ERC20EmptyMetadataFactory.deploy(18); + await emptyToken.waitForDeployment(); + const emptyTokenAddress = await emptyToken.getAddress(); + + await emptyToken.mint(signers.alice, 1000); + + const { cToken, receipt } = await deployConfidentialToken(coordinator, emptyToken, signers.alice); + + // Verify deployment succeeded + expect(await coordinator.wrapperExists(emptyTokenAddress)).to.equal(true); + + // Verify fallback to address-based naming + const expectedName = ethers.toBeHex(BigInt(emptyTokenAddress), 20); + const expectedSymbol = expectedName.slice(0, 8); // "0x" + 6 chars + + expect(await cToken.name()).to.equal(`confidential ${expectedName}`); + expect(await cToken.symbol()).to.equal(`c${expectedSymbol}`); + + // Verify event uses fallback names + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent[0].args[3]).to.equal(expectedName); + expect(wrapDeployedEvent[0].args[4]).to.equal(expectedSymbol); + }); + }); + + describe("ERC20 with no metadata functions", function () { + it("should deploy wrapper using address fallback when metadata functions don't exist", async function () { + const ERC20NoMetadataFactory = await ethers.getContractFactory("ERC20NoMetadata"); + const noMetadataToken = await ERC20NoMetadataFactory.deploy(); + await noMetadataToken.waitForDeployment(); + const noMetadataTokenAddress = await noMetadataToken.getAddress(); + + await noMetadataToken.mint(signers.alice, 1000); + + const { cToken, receipt } = await deployConfidentialToken(coordinator, noMetadataToken, signers.alice); + + // Verify deployment succeeded + expect(await coordinator.wrapperExists(noMetadataTokenAddress)).to.equal(true); + + // Verify fallback to address-based naming + const expectedName = ethers.toBeHex(BigInt(noMetadataTokenAddress), 20); + const expectedSymbol = expectedName.slice(0, 8); // "0x" + 6 chars + + expect(await cToken.name()).to.equal(`confidential ${expectedName}`); + expect(await cToken.symbol()).to.equal(`c${expectedSymbol}`); + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); // Uses fallback decimals + + // Verify event uses fallback names + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent[0].args[3]).to.equal(expectedName); + expect(wrapDeployedEvent[0].args[4]).to.equal(expectedSymbol); + expect(wrapDeployedEvent[0].args[5]).to.equal(18); // Fallback decimals + }); + }); + + describe("Standard ERC20 compatibility verification", function () { + it("should still work correctly with standard ERC20 tokens", async function () { + // Use the standard USDC from the fixture + const { cToken, receipt } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Verify standard token behavior is not affected + expect(await cToken.name()).to.equal("confidential USDC"); + expect(await cToken.symbol()).to.equal("cUSDC"); + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); + + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent[0].args[3]).to.equal("USDC"); + expect(wrapDeployedEvent[0].args[4]).to.equal("USDC"); + }); + }); + + describe("Address fallback format", function () { + it("should always produce fixed-width 42-char hex string (0x + 40 hex) for address fallback", async function () { + const ERC20RevertingMetadataFactory = await ethers.getContractFactory("ERC20RevertingMetadata"); + const revertingToken = await ERC20RevertingMetadataFactory.deploy(18); + await revertingToken.waitForDeployment(); + const revertingTokenAddress = await revertingToken.getAddress(); + + await revertingToken.mint(signers.alice, 1000); + + const { cToken, receipt } = await deployConfidentialToken(coordinator, revertingToken, signers.alice); + + // Verify deployment succeeded + expect(await coordinator.wrapperExists(revertingTokenAddress)).to.equal(true); + + // Verify fallback name is ALWAYS 42 characters (0x + 40 hex) + const name = await cToken.name(); + const nameWithoutPrefix = name.replace("confidential ", ""); + expect(nameWithoutPrefix.length).to.equal(42); + expect(nameWithoutPrefix).to.match(/^0x[0-9a-f]{40}$/); + + // Verify fallback symbol is ALWAYS 8 characters (0x + 6 hex) + const symbol = await cToken.symbol(); + const symbolWithoutPrefix = symbol.replace("c", ""); + expect(symbolWithoutPrefix.length).to.equal(8); + expect(symbolWithoutPrefix).to.match(/^0x[0-9a-f]{6}$/); + + // Verify consistency with ethers.toBeHex + const expectedName = ethers.toBeHex(BigInt(revertingTokenAddress), 20); + const expectedSymbol = expectedName.slice(0, 8); + expect(nameWithoutPrefix).to.equal(expectedName); + expect(symbolWithoutPrefix).to.equal(expectedSymbol); + + // Verify event uses fallback names + const wrapDeployedEvent = getWrapDeployedEvent(receipt); + expect(wrapDeployedEvent[0].args[3]).to.equal(expectedName); + expect(wrapDeployedEvent[0].args[4]).to.equal(expectedSymbol); + }); + + it("should handle address with leading zeros correctly", async function () { + // While we can't deploy to a specific low address in tests, + // we can verify that any address produces a 42-char string + const ERC20EmptyMetadataFactory = await ethers.getContractFactory("ERC20EmptyMetadata"); + const emptyToken = await ERC20EmptyMetadataFactory.deploy(18); + await emptyToken.waitForDeployment(); + const emptyTokenAddress = await emptyToken.getAddress(); + + await emptyToken.mint(signers.alice, 1000); + + const { cToken } = await deployConfidentialToken(coordinator, emptyToken, signers.alice); + + // The key property: name must ALWAYS be 42 chars, even for low addresses + const name = await cToken.name(); + const nameWithoutPrefix = name.replace("confidential ", ""); + + // This ensures leading zeros are preserved + expect(nameWithoutPrefix.length).to.equal(42, "Name should be exactly 42 characters (0x + 40 hex)"); + expect(nameWithoutPrefix.startsWith("0x")).to.be.true; + + // Verify symbol can be extracted without revert (the bug scenario) + const symbol = await cToken.symbol(); + const symbolWithoutPrefix = symbol.replace("c", ""); + expect(symbolWithoutPrefix.length).to.equal(8, "Symbol should be exactly 8 characters (0x + 6 hex)"); + + // Verify format is lowercase hex + expect(nameWithoutPrefix).to.match(/^0x[0-9a-f]{40}$/); + expect(symbolWithoutPrefix).to.match(/^0x[0-9a-f]{6}$/); + }); + }); + + describe("Mixed edge cases", function () { + it("should handle bytes32 with all 32 bytes used", async function () { + const ERC20Bytes32MetadataFactory = await ethers.getContractFactory("ERC20Bytes32Metadata"); + + // Create a 32-char string (max length for bytes32) + const longName = "VeryLongTokenNameExactly32ch"; + const nameBytes32 = ethers.encodeBytes32String(longName); + const symbolBytes32 = ethers.encodeBytes32String("LONG"); + + const longNameToken = await ERC20Bytes32MetadataFactory.deploy( + nameBytes32, + symbolBytes32, + 6 + ); + await longNameToken.waitForDeployment(); + + const { cToken } = await deployConfidentialToken(coordinator, longNameToken, signers.alice); + + expect(await cToken.name()).to.equal(`confidential ${longName}`); + expect(await cToken.symbol()).to.equal("cLONG"); + expect(await cToken.decimals()).to.equal(MAX_DECIMALS); + }); + + it("should handle bytes32 with zero value", async function () { + const ERC20Bytes32MetadataFactory = await ethers.getContractFactory("ERC20Bytes32Metadata"); + + // bytes32(0) - empty + const zeroBytes32 = ethers.ZeroHash; + + const zeroToken = await ERC20Bytes32MetadataFactory.deploy( + zeroBytes32, + zeroBytes32, + 18 + ); + await zeroToken.waitForDeployment(); + const zeroTokenAddress = await zeroToken.getAddress(); + + const { cToken } = await deployConfidentialToken(coordinator, zeroToken, signers.alice); + + // Should fallback to address-based naming + const expectedName = ethers.toBeHex(BigInt(zeroTokenAddress), 20); + const expectedSymbol = expectedName.slice(0, 8); + + expect(await cToken.name()).to.equal(`confidential ${expectedName}`); + expect(await cToken.symbol()).to.equal(`c${expectedSymbol}`); + }); + }); + }); + + describe("Upgradeability", function () { + it("should allow admin to upgrade wrapper to V2", async function () { + // Deploy a wrapper using the coordinator + const usdc = await deployTestERC20Fixture("USDC", 6); + const { wrapper, cToken } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapperAddress = await wrapper.getAddress(); + + // Verify current implementation + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const implBefore = await ethers.provider.getStorage(wrapperAddress, implementationSlot); + const implBeforeAddress = "0x" + implBefore.slice(26); + + const wrapperImplementation = await ethers.getContractAt("FeeManager", await coordinator.wrapperImplementation()); + expect(implBeforeAddress.toLowerCase()).to.equal((await wrapperImplementation.getAddress()).toLowerCase()); + + // Deploy V2 implementation + const WrapperV2 = await ethers.getContractFactory("WrapperUpgradeableV2Mock"); + const v2Impl = await WrapperV2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Verify deployer has DEFAULT_ADMIN_ROLE + const adminRole = await wrapper.DEFAULT_ADMIN_ROLE(); + expect(await wrapper.hasRole(adminRole, signers.deployer.address)).to.be.true; + + // Grant UPGRADER_ROLE to deployer + const upgraderRole = await wrapper.UPGRADER_ROLE(); + await wrapper.connect(signers.deployer).grantRole(upgraderRole, signers.deployer.address); + + // Upgrade to V2 + await wrapper.connect(signers.deployer).upgradeToAndCall(v2ImplAddress, "0x"); + + // Verify implementation was upgraded + const implAfter = await ethers.provider.getStorage(wrapperAddress, implementationSlot); + const implAfterAddress = "0x" + implAfter.slice(26); + expect(implAfterAddress.toLowerCase()).to.equal(v2ImplAddress.toLowerCase()); + + // Verify V2 functionality works + const wrapperV2 = WrapperV2.attach(wrapperAddress); + expect(await wrapperV2.counter()).to.equal(0); + await wrapperV2.incrementCounter(); + expect(await wrapperV2.counter()).to.equal(1); + + // Verify original functionality still works + expect(await wrapper.confidentialToken()).to.equal(await cToken.getAddress()); + expect(await wrapper.originalToken()).to.equal(await usdc.getAddress()); + }); + + it("should prevent non-admin from upgrading wrapper", async function () { + // Deploy a wrapper using the coordinator + const usdc = await deployTestERC20Fixture("USDC", 6); + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Deploy V2 implementation + const WrapperV2 = await ethers.getContractFactory("WrapperUpgradeableV2Mock"); + const v2Impl = await WrapperV2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Try to upgrade as bob (doesn't have UPGRADER_ROLE) + const upgraderRole = await wrapper.UPGRADER_ROLE(); + await expect( + wrapper.connect(signers.bob).upgradeToAndCall(v2ImplAddress, "0x") + ).to.be.revertedWithCustomError(wrapper, "AccessControlUnauthorizedAccount") + .withArgs(signers.bob.address, upgraderRole); + }); + + it("should maintain independent upgradeability for multiple wrappers", async function () { + // Deploy two wrappers + const usdc = await deployTestERC20Fixture("USDC", 6); + const dai = await deployTestERC20Fixture("DAI", 18); + + const wrapperImplementation = await ethers.getContractAt("FeeManager", await coordinator.wrapperImplementation()); + + const { wrapper: wrapper1 } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const { wrapper: wrapper2 } = await deployConfidentialToken(coordinator, dai, signers.alice); + + const wrapper1Address = await wrapper1.getAddress(); + const wrapper2Address = await wrapper2.getAddress(); + + // Verify both use original implementation + const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + const impl1Before = await ethers.provider.getStorage(wrapper1Address, implementationSlot); + const impl1BeforeAddress = "0x" + impl1Before.slice(26); + const impl2Before = await ethers.provider.getStorage(wrapper2Address, implementationSlot); + const impl2BeforeAddress = "0x" + impl2Before.slice(26); + + expect(impl1BeforeAddress.toLowerCase()).to.equal((await wrapperImplementation.getAddress()).toLowerCase()); + expect(impl2BeforeAddress.toLowerCase()).to.equal((await wrapperImplementation.getAddress()).toLowerCase()); + + // Deploy V2 implementation + const WrapperV2 = await ethers.getContractFactory("WrapperUpgradeableV2Mock"); + const v2Impl = await WrapperV2.deploy(); + await v2Impl.waitForDeployment(); + const v2ImplAddress = await v2Impl.getAddress(); + + // Grant UPGRADER_ROLE to deployer for wrapper1 + const upgraderRole = await wrapper1.UPGRADER_ROLE(); + await wrapper1.connect(signers.deployer).grantRole(upgraderRole, signers.deployer.address); + + // Upgrade ONLY wrapper1 to V2 + await wrapper1.connect(signers.deployer).upgradeToAndCall(v2ImplAddress, "0x"); + + // Verify wrapper1 was upgraded + const impl1After = await ethers.provider.getStorage(wrapper1Address, implementationSlot); + const impl1AfterAddress = "0x" + impl1After.slice(26); + expect(impl1AfterAddress.toLowerCase()).to.equal(v2ImplAddress.toLowerCase()); + + // Verify wrapper2 still uses original implementation + const impl2After = await ethers.provider.getStorage(wrapper2Address, implementationSlot); + const impl2AfterAddress = "0x" + impl2After.slice(26); + expect(impl2AfterAddress.toLowerCase()).to.equal((await wrapperImplementation.getAddress()).toLowerCase()); + + // Verify V2 functionality works on wrapper1 + const wrapper1V2 = WrapperV2.attach(wrapper1Address); + expect(await wrapper1V2.counter()).to.equal(0); + await wrapper1V2.incrementCounter(); + expect(await wrapper1V2.counter()).to.equal(1); + + // Verify wrapper2 doesn't have V2 functions (still V1) + const wrapper2AsV2 = WrapperV2.attach(wrapper2Address); + await expect(wrapper2AsV2.counter()).to.be.reverted; // V2 functions don't exist on V1 + + // Verify both wrappers still have correct original tokens + expect(await wrapper1.originalToken()).to.equal(await usdc.getAddress()); + expect(await wrapper2.originalToken()).to.equal(await dai.getAddress()); + }); + + it("should prevent implementation contract from initializing", async function () { + const WrapperUpgradeableFactory = await ethers.getContractFactory("WrapperUpgradeable"); + const wrapperImpl = await WrapperUpgradeableFactory.deploy(); + await wrapperImpl.waitForDeployment(); + + const usdc = await deployTestERC20Fixture("USDC", 6); + const { cToken } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + await expect( + wrapperImpl.initialize( + await usdc.getAddress(), + cToken, + adminProvider, + signers.deployer.address + ) + ).to.be.revertedWithCustomError(WrapperUpgradeableFactory, "InvalidInitialization"); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Wrapper.unwrap.ts b/protocol-contracts/confidential-wrapper/test/Wrapper.unwrap.ts new file mode 100644 index 0000000000..de04c147a1 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Wrapper.unwrap.ts @@ -0,0 +1,1788 @@ +import { ethers, fhevm } from "hardhat"; +import { expect } from "chai"; +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { + deployConfidentialToken, + deployConfidentialETH, + getEncryptedBalance, + unwrapToken, + verifyWrapperBacking, + wrapETH, + wrapERC20, + getUnwrapFinalizedEvent, + getBurnEvent, + getUnwrapStartedEvent, + checkTotalSupply, +} from "./utils"; +import { getSigners, Signers } from "./signers"; +import type { TestERC20, AdminProvider, DeploymentCoordinator, FeeManager, ERC20FailOnAddressMock, WrapperReceiverMock } from "../types"; +import { deployWrapperFixture, deployTestERC20Fixture, deployTestUnsafeERC20Fixture } from "./fixtures"; + +async function deployFixture(signers: Signers) { + const { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory } = await deployWrapperFixture(signers); + + const usdc = await deployTestERC20Fixture("USDC"); + const usdcAddress = await usdc.getAddress(); + + const transaction = await usdc.mint(signers.alice, ethers.parseUnits("100000", 6)); + await transaction.wait(); + + return { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory, usdc, usdcAddress }; +} + +describe("Wrapper Unwrap Flow", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let adminProvider: AdminProvider; + let usdc: TestERC20; + let feeManager: FeeManager; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + ({ + coordinator, + adminProvider, + usdc, + } = await deployFixture(signers)); + + const feeManagerAddress = await adminProvider.feeManager(); + feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + }); + + describe("ETH Unwrapping with Fees", function () { + it("should unwrap ETH with fee - comprehensive balance checks", async function () { + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper, wrapperAddress } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Verify initial backing + await verifyWrapperBacking(wrapper); + + // Get initial balances + const aliceEthBefore = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + + // Get alice's cETH balance + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + const rate = await cEth.rate(); + + // CHECK: requestId before unwrap + const requestIdBefore = await wrapper.requestId(); + + // UNWRAP + const { unwrapReceipt, unwrapFinalizedReceipt, unwrapFee } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCEthBalance, + signers.alice + ); + + // CHECK: requestId was incremented after onConfidentialTransferReceived + const requestIdAfter = await wrapper.requestId(); + expect(requestIdAfter).to.equal(requestIdBefore + 1n); + + // Calculate expected amounts + const expectedUnwrapAmount = (aliceCEthBalance - unwrapFee) * rate; + const expectedFeeAmount = unwrapFee * rate; + + // CHECK: alice ETH balance increased + const aliceEthAfter = await ethers.provider.getBalance(signers.alice.address); + const gasUsed = ( + (unwrapReceipt?.gasPrice ?? 0n) * (unwrapReceipt?.gasUsed ?? 0n) + + (unwrapFinalizedReceipt?.gasPrice ?? 0n) * (unwrapFinalizedReceipt?.gasUsed ?? 0n) + ); + + // CHECK: burn event + const burnEvents = getBurnEvent(unwrapReceipt); + expect(burnEvents.length).to.be.equal(1); + expect(burnEvents[0].args[0]).to.equal(signers.alice.address); + const burnAmountHandle = burnEvents[0].args[1]; + const burnAmount = await fhevm.userDecryptEuint( + FhevmType.euint64, + burnAmountHandle, + cEth, + signers.alice, + ); + expect(burnAmount).to.be.equal(aliceCEthBalance); + + // CHECK: unwrap started event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.be.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + expect(unwrapStartedEvent.args[0]).to.be.equal(true); + const actualRequestId = unwrapStartedEvent.args[1]; + const expectedRequestId = 0; + expect(actualRequestId).to.be.equal(expectedRequestId); + + // CHECK: unwrap finalized event + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.be.equal(1); + const unwrapFinalizedEvent = unwrapFinalizedEvents[0]; + expect(unwrapFinalizedEvent.args[0]).to.be.equal(actualRequestId); + expect(unwrapFinalizedEvent.args[1]).to.be.equal(true); + expect(unwrapFinalizedEvent.args[2]).to.be.equal(true); + expect(unwrapFinalizedEvent.args[3]).to.be.equal(aliceCEthBalance); + expect(unwrapFinalizedEvent.args[4]).to.be.equal(expectedUnwrapAmount); + expect(unwrapFinalizedEvent.args[5]).to.be.equal(unwrapFee * rate); + expect(unwrapFinalizedEvent.args[6]).to.be.equal(await cEth.nextTxId()); + + // Alice should receive unwrapAmount minus gas + expect(aliceEthAfter - aliceEthBefore).to.equal(expectedUnwrapAmount - gasUsed); + + // CHECK: royalties received fee in ETH + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(expectedFeeAmount); + + // CHECK: wrapper balance decreased + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + expect(wrapperEthBefore - wrapperEthAfter).to.equal(expectedUnwrapAmount + expectedFeeAmount); + + // CHECK: alice cETH balance is zero + expect(await getEncryptedBalance(cEth, signers.alice, cEthAddress)).to.equal(0); + + // VERIFY: Wrapper backing invariant (should be zero now) + await verifyWrapperBacking(wrapper); + + // VERIFY: Total accounting + const totalEthOut = expectedUnwrapAmount + expectedFeeAmount; + expect(wrapperEthBefore - wrapperEthAfter).to.equal(totalEthOut); + }); + + it("should unwrap ETH without fee - comprehensive balance checks", async function () { + // SETTINGS: remove unwrap fee + await feeManager.setUnwrapFeeBasisPoints(0); + + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Get initial balances + const aliceEthBefore = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + const rate = await cEth.rate(); + + // UNWRAP + const { unwrapReceipt, unwrapFinalizedReceipt, unwrapFee } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCEthBalance, + signers.alice + ); + + expect(unwrapFee).to.equal(0n); + + const expectedUnwrapAmount = aliceCEthBalance * rate; + + // CHECK: alice ETH balance increased by full amount + const aliceEthAfter = await ethers.provider.getBalance(signers.alice.address); + const gasUsed = ( + (unwrapReceipt?.gasPrice ?? 0n) * (unwrapReceipt?.gasUsed ?? 0n) + + (unwrapFinalizedReceipt?.gasPrice ?? 0n) * (unwrapFinalizedReceipt?.gasUsed ?? 0n) + ); + + expect(aliceEthAfter - aliceEthBefore).to.equal(expectedUnwrapAmount - gasUsed); + + // CHECK: royalties received no fee + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(0n); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle ETH unwrap when fee recipient rejects ETH", async function () { + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Deploy RejectEth and set as fee recipient + const RejectEthContract = await ethers.getContractFactory("RejectEth"); + const rejectEth = await RejectEthContract.deploy(); + await rejectEth.waitForDeployment(); + await feeManager.setFeeRecipient(await rejectEth.getAddress()); + + // Get balances + const aliceEthBefore = await ethers.provider.getBalance(signers.alice.address); + + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + const rate = await cEth.rate(); + + // UNWRAP - should succeed, alice gets fee + unwrap amount + const { unwrapReceipt, unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCEthBalance, + signers.alice + ); + + // When fee transfer fails, unwrapAmount should include the fee + // Protocol takes the hit to maintain parity + const expectedTotalAmount = aliceCEthBalance * rate; + + const aliceEthAfter = await ethers.provider.getBalance(signers.alice.address); + const gasUsed = ( + (unwrapReceipt?.gasPrice ?? 0n) * (unwrapReceipt?.gasUsed ?? 0n) + + (unwrapFinalizedReceipt?.gasPrice ?? 0n) * (unwrapFinalizedReceipt?.gasUsed ?? 0n) + ); + + expect(aliceEthAfter - aliceEthBefore).to.equal(expectedTotalAmount - gasUsed); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + + // VERIFY: RejectEth received nothing + expect(await ethers.provider.getBalance(await rejectEth.getAddress())).to.equal(0n); + }); + + it.skip("should handle ETH unwrap when unwrap destination rejects ETH (fee succeeds)", async function () { + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper, wrapperAddress } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Deploy RejectEth contract as unwrap destination + const RejectEthContract = await ethers.getContractFactory("RejectEth"); + const rejectEth = await RejectEthContract.deploy(); + await rejectEth.waitForDeployment(); + const rejectEthAddress = await rejectEth.getAddress(); + + // Get initial balances + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + + const aliceCEthBalanceBefore = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + const rate = await cEth.rate(); + + // UNWRAP to RejectEth address - fee succeeds, unwrap fails + const { unwrapFinalizedReceipt, unwrapFee } = await unwrapToken( + wrapper, + rejectEthAddress, // This address rejects ETH + aliceCEthBalanceBefore, + signers.alice + ); + + const expectedFeeAmount = unwrapFee * rate; + + // CHECK: Fee recipient received fee + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(expectedFeeAmount); + + // CHECK: Wrapper lost only the fee amount + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + expect(wrapperEthBefore - wrapperEthAfter).to.equal(expectedFeeAmount); + + // CHECK: RejectEth received nothing + expect(await ethers.provider.getBalance(rejectEthAddress)).to.equal(0n); + + // CHECK: Alice got reminted (actualBurnAmount - feeAmount64) cTokens + const aliceCEthBalanceAfter = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + expect(aliceCEthBalanceAfter).to.equal(aliceCEthBalanceBefore - unwrapFee); + + // CHECK: UnwrapFinalized event shows partial failure + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(false, "ethUnwrapSuccess should be false"); + expect(event.args[2]).to.equal(true, "ethFeeSuccess should be true"); + expect(event.args[3]).to.be.equal(aliceCEthBalanceBefore); + expect(event.args[4]).to.be.equal(0); + expect(event.args[5]).to.be.equal(unwrapFee * rate); + expect(event.args[6]).to.be.equal((await cEth.nextTxId()) - BigInt(1)); + + // VERIFY: Wrapper backing invariant (parity maintained) + await verifyWrapperBacking(wrapper); + }); + + it.skip("should handle ETH unwrap when both fee and unwrap destinations reject ETH", async function () { + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper, wrapperAddress } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Deploy RejectEth contracts + const RejectEthContract = await ethers.getContractFactory("RejectEth"); + const rejectEthFee = await RejectEthContract.deploy(); + await rejectEthFee.waitForDeployment(); + const rejectEthUnwrap = await RejectEthContract.deploy(); + await rejectEthUnwrap.waitForDeployment(); + + // Set RejectEth as fee recipient + await feeManager.setFeeRecipient(await rejectEthFee.getAddress()); + + // Get initial balances + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + const aliceCEthBalanceBefore = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + + // UNWRAP to RejectEth address - both fail + const { unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + await rejectEthUnwrap.getAddress(), // This address rejects ETH + aliceCEthBalanceBefore, + signers.alice + ); + + // CHECK: Wrapper ETH unchanged (both transfers failed) + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + expect(wrapperEthAfter).to.equal(wrapperEthBefore); + + // CHECK: Both RejectEth contracts received nothing + expect(await ethers.provider.getBalance(await rejectEthFee.getAddress())).to.equal(0n); + expect(await ethers.provider.getBalance(await rejectEthUnwrap.getAddress())).to.equal(0n); + + // CHECK: Alice got ALL cTokens reminted (actualBurnAmount) + const aliceCEthBalanceAfter = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + expect(aliceCEthBalanceAfter).to.equal(aliceCEthBalanceBefore); + + // CHECK: UnwrapFinalized event shows both failed + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(false, "ethUnwrapSuccess should be false"); + expect(event.args[2]).to.equal(false, "ethFeeSuccess should be false"); + expect(event.args[3]).to.equal(aliceCEthBalanceBefore); + expect(event.args[4]).to.equal(0n, "unwrapAmount should be 0"); + expect(event.args[5]).to.equal(0n, "feeAmount should be 0"); + expect(event.args[6]).to.be.equal((await cEth.nextTxId()) - BigInt(1)); + + // VERIFY: Wrapper backing invariant (parity maintained) + await verifyWrapperBacking(wrapper); + }); + + it("should handle various unwrap amounts", async function () { + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + const testAmounts = [ + ethers.parseEther("0.001"), + ethers.parseEther("0.1"), + ethers.parseEther("1.0"), + ethers.parseEther("10.0"), + ]; + + for (const wrapAmount of testAmounts) { + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + + await unwrapToken( + wrapper, + signers.alice.address, + aliceCEthBalance, + signers.alice + ); + + // Verify parity after each unwrap + await verifyWrapperBacking(wrapper); + await checkTotalSupply(cEth, 0); + } + }); + }); + + describe("ERC20 Unwrapping with Fees", function () { + it("should unwrap USDC with fee - comprehensive balance checks", async function () { + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Verify initial backing + await verifyWrapperBacking(wrapper); + + // Get initial balances + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcBefore = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcBefore = await usdc.balanceOf(wrapperAddress); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // CHECK: requestId before unwrap + const requestIdBefore = await wrapper.requestId(); + + // UNWRAP + const { unwrapFee } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalance, + signers.alice + ); + + // CHECK: requestId was incremented after onConfidentialTransferReceived + const requestIdAfter = await wrapper.requestId(); + expect(requestIdAfter).to.equal(requestIdBefore + 1n); + + // Calculate expected amounts + const expectedUnwrapAmount = (aliceCUsdcBalance - unwrapFee) * rate; + const expectedFeeAmount = unwrapFee * rate; + + // CHECK: alice USDC balance increased + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + expect(aliceUsdcAfter - aliceUsdcBefore).to.equal(expectedUnwrapAmount); + + // CHECK: royalties received fee in USDC + const royaltiesUsdcAfter = await usdc.balanceOf(signers.royalties.address); + expect(royaltiesUsdcAfter - royaltiesUsdcBefore).to.equal(expectedFeeAmount); + + // CHECK: wrapper balance decreased + const wrapperUsdcAfter = await usdc.balanceOf(wrapperAddress); + expect(wrapperUsdcBefore - wrapperUsdcAfter).to.equal(expectedUnwrapAmount + expectedFeeAmount); + + // CHECK: alice cUSDC balance is zero + expect( + await getEncryptedBalance(cToken, signers.alice, cTokenAddress) + ).to.equal(0); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + + // VERIFY: No tokens created or destroyed + const totalUsdcOut = (aliceUsdcAfter - aliceUsdcBefore) + (royaltiesUsdcAfter - royaltiesUsdcBefore); + expect(wrapperUsdcBefore - wrapperUsdcAfter).to.equal(totalUsdcOut); + }); + + it("should unwrap USDC without fee", async function () { + // SETTINGS: remove unwrap fee + await feeManager.setUnwrapFeeBasisPoints(0); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // UNWRAP + const { unwrapFee } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalance, + signers.alice + ); + + expect(unwrapFee).to.equal(0n); + + const expectedUnwrapAmount = aliceCUsdcBalance * rate; + + // CHECK: alice USDC balance increased by full amount + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + expect(aliceUsdcAfter - aliceUsdcBefore).to.equal(expectedUnwrapAmount); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle various unwrap amounts", async function () { + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const testAmounts = [ + BigInt(100), + BigInt(1000), + BigInt(10000), + BigInt(100000), + ]; + + for (const wrapAmount of testAmounts) { + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalance, + signers.alice + ); + + // Verify parity after each unwrap + await verifyWrapperBacking(wrapper); + } + }); + + for (let unsafeERC20Name of ["ERC20FailOnAddressMock", "ERC20RevertOnAddressMock"]) { + it(`should handle ERC20 unwrap when fee recipient rejects tokens with ${unsafeERC20Name}`, async function () { + // Deploy mock token that fails on transfers to fee recipient + const mockUsdcAddress = await (await deployTestUnsafeERC20Fixture(unsafeERC20Name, "Mock USDC", 6)).getAddress(); + const mockUsdc = (await ethers.getContractAt(unsafeERC20Name, mockUsdcAddress)) as ERC20FailOnAddressMock; + await mockUsdc.mint(signers.alice, 200_000); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, mockUsdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, mockUsdc, wrapAmount, signers.alice.address, signers.alice); + + // Configure mock to fail on transfers to fee recipient + const feeRecipient = await feeManager.getFeeRecipient(); + await mockUsdc.setFailOnTransferTo(feeRecipient, true); + + // Get initial balances + const aliceUsdcBefore = await mockUsdc.balanceOf(signers.alice.address); + const wrapperUsdcBefore = await mockUsdc.balanceOf(wrapperAddress); + const royaltiesUsdcBefore = await mockUsdc.balanceOf(feeRecipient); + const aliceCUsdcBalanceBefore = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // UNWRAP - fee transfer fails, but unwrap succeeds (protocol absorbs fee loss) + const { unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalanceBefore, + signers.alice + ); + + const expectedTotalAmount = aliceCUsdcBalanceBefore * rate; + + // CHECK: Alice received total amount (unwrap + fee, protocol absorbed loss) + const aliceUsdcAfter = await mockUsdc.balanceOf(signers.alice.address); + expect(aliceUsdcAfter - aliceUsdcBefore).to.equal(expectedTotalAmount); + + // CHECK: Wrapper lost everything + const wrapperUsdcAfter = await mockUsdc.balanceOf(wrapperAddress); + expect(wrapperUsdcBefore - wrapperUsdcAfter).to.equal(expectedTotalAmount); + + // CHECK: Fee recipient received nothing (transfer failed) + const royaltiesUsdcAfter = await mockUsdc.balanceOf(feeRecipient); + expect(royaltiesUsdcAfter).to.equal(royaltiesUsdcBefore); + + // CHECK: Alice has no cTokens left (not reminted, since unwrap succeeded) + const aliceCUsdcBalanceAfter = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(aliceCUsdcBalanceAfter).to.equal(0n); + + // CHECK: UnwrapFinalized event + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(true, "unwrapSuccess should be true"); + expect(event.args[2]).to.equal(false, "feeSuccess should be false"); + expect(event.args[4]).to.equal(expectedTotalAmount, "unwrapAmount includes fee"); + expect(event.args[5]).to.equal(0n, "feeAmount should be 0"); + expect(event.args[6]).to.be.equal((await cToken.nextTxId()), "no reimbursement"); + + // VERIFY: Wrapper backing invariant maintained + await verifyWrapperBacking(wrapper); + }); + + it(`should handle ERC20 unwrap when unwrap destination rejects tokens (fee succeeds) with ${unsafeERC20Name}`, async function () { + // Deploy mock token that fails on transfers to alice + const mockUsdcAddress = await (await deployTestUnsafeERC20Fixture("ERC20FailOnAddressMock", "Mock USDC", 6)).getAddress(); + const mockUsdc = (await ethers.getContractAt("ERC20FailOnAddressMock", mockUsdcAddress)) as ERC20FailOnAddressMock; + await mockUsdc.mint(signers.alice, 200_000); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, mockUsdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, mockUsdc, wrapAmount, signers.alice.address, signers.alice); + + // Configure mock to fail on transfers to alice + await mockUsdc.setFailOnTransferTo(signers.alice.address, true); + + // Get initial balances + const aliceUsdcBefore = await mockUsdc.balanceOf(signers.alice.address); + const wrapperUsdcBefore = await mockUsdc.balanceOf(wrapperAddress); + const royaltiesUsdcBefore = await mockUsdc.balanceOf(signers.royalties.address); + const aliceCUsdcBalanceBefore = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // UNWRAP - fee succeeds, unwrap fails + const { unwrapFinalizedReceipt, unwrapFee } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalanceBefore, + signers.alice + ); + + const expectedFeeAmount = unwrapFee * rate; + + // CHECK: Alice received nothing (transfer failed) + const aliceUsdcAfter = await mockUsdc.balanceOf(signers.alice.address); + expect(aliceUsdcAfter).to.equal(aliceUsdcBefore); + + // CHECK: Wrapper lost only fee amount + const wrapperUsdcAfter = await mockUsdc.balanceOf(wrapperAddress); + expect(wrapperUsdcBefore - wrapperUsdcAfter).to.equal(expectedFeeAmount); + + // CHECK: Fee recipient received fee + const royaltiesUsdcAfter = await mockUsdc.balanceOf(signers.royalties.address); + expect(royaltiesUsdcAfter - royaltiesUsdcBefore).to.equal(expectedFeeAmount); + + // CHECK: Alice got reminted principal only (actualBurnAmount - feeAmount64) + const aliceCUsdcBalanceAfter = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(aliceCUsdcBalanceAfter).to.equal(aliceCUsdcBalanceBefore - unwrapFee); + + // CHECK: UnwrapFinalized event + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(false, "unwrapSuccess should be false"); + expect(event.args[2]).to.equal(true, "feeSuccess should be true"); + expect(event.args[4]).to.equal(0n, "unwrapAmount should be 0"); + expect(event.args[5]).to.equal(expectedFeeAmount, "feeAmount"); + expect(event.args[6]).to.be.equal((await cToken.nextTxId()) - BigInt(1)); + + // VERIFY: Wrapper backing invariant maintained + await verifyWrapperBacking(wrapper); + }); + + it(`should handle ERC20 unwrap when both transfers fail with ${unsafeERC20Name}`, async function () { + // Deploy mock token that fails on transfers to both fee recipient and alice + const mockUsdcAddress = await (await deployTestUnsafeERC20Fixture("ERC20FailOnAddressMock", "Mock USDC", 6)).getAddress(); + const mockUsdc = (await ethers.getContractAt("ERC20FailOnAddressMock", mockUsdcAddress)) as ERC20FailOnAddressMock; + await mockUsdc.mint(signers.alice, 200_000); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, mockUsdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, mockUsdc, wrapAmount, signers.alice.address, signers.alice); + + // Configure mock to fail on transfers to both + const feeRecipient = await feeManager.getFeeRecipient(); + await mockUsdc.setFailOnTransferTo(feeRecipient, true); + await mockUsdc.setFailOnTransferTo(signers.alice.address, true); + + // Get initial balances + const aliceUsdcBefore = await mockUsdc.balanceOf(signers.alice.address); + const wrapperUsdcBefore = await mockUsdc.balanceOf(wrapperAddress); + const royaltiesUsdcBefore = await mockUsdc.balanceOf(feeRecipient); + const aliceCUsdcBalanceBefore = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // UNWRAP - both fail + const { unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalanceBefore, + signers.alice + ); + + // CHECK: Alice USDC unchanged + const aliceUsdcAfter = await mockUsdc.balanceOf(signers.alice.address); + expect(aliceUsdcAfter).to.equal(aliceUsdcBefore); + + // CHECK: Wrapper USDC unchanged (both transfers failed) + const wrapperUsdcAfter = await mockUsdc.balanceOf(wrapperAddress); + expect(wrapperUsdcAfter).to.equal(wrapperUsdcBefore); + + // CHECK: Fee recipient USDC unchanged + const royaltiesUsdcAfter = await mockUsdc.balanceOf(feeRecipient); + expect(royaltiesUsdcAfter).to.equal(royaltiesUsdcBefore); + + // CHECK: Alice got ALL cTokens reminted (actualBurnAmount) + const aliceCUsdcBalanceAfter = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(aliceCUsdcBalanceAfter).to.equal(aliceCUsdcBalanceBefore); + + // CHECK: UnwrapFinalized event shows both failed + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(false, "unwrapSuccess should be false"); + expect(event.args[2]).to.equal(false, "feeSuccess should be false"); + expect(event.args[4]).to.equal(0n, "unwrapAmount should be 0"); + expect(event.args[5]).to.equal(0n, "feeAmount should be 0"); + expect(event.args[6]).to.be.equal((await cToken.nextTxId()) - BigInt(1)); + + // VERIFY: Wrapper backing invariant maintained + await verifyWrapperBacking(wrapper); + }); + } + }); + + describe("Unwrap to Different Recipient", function () { + it("should unwrap ETH to bob", async function () { + // DEPLOY & WRAP + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + const bobEthBefore = await ethers.provider.getBalance(signers.bob.address); + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + const rate = await cEth.rate(); + + // UNWRAP to bob + const { unwrapFee } = await unwrapToken( + wrapper, + signers.bob.address, // Different recipient + aliceCEthBalance, + signers.alice + ); + + const expectedUnwrapAmount = (aliceCEthBalance - unwrapFee) * rate; + + // CHECK: bob received the ETH + const bobEthAfter = await ethers.provider.getBalance(signers.bob.address); + expect(bobEthAfter - bobEthBefore).to.equal(expectedUnwrapAmount); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should unwrap USDC to bob", async function () { + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const bobUsdcBefore = await usdc.balanceOf(signers.bob.address); + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // UNWRAP to bob + const { unwrapFee } = await unwrapToken( + wrapper, + signers.bob.address, // Different recipient + aliceCUsdcBalance, + signers.alice + ); + + const expectedUnwrapAmount = (aliceCUsdcBalance - unwrapFee) * rate; + + // CHECK: bob received the USDC + const bobUsdcAfter = await usdc.balanceOf(signers.bob.address); + expect(bobUsdcAfter - bobUsdcBefore).to.equal(expectedUnwrapAmount); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + }); + + describe("Edge Cases", function () { + it("should handle unwrap of 0 tokens", async function () { + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Get initial balances + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcBefore = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcBefore = await usdc.balanceOf(wrapperAddress); + const aliceCTokenBefore = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // UNWRAP 0 tokens + const { unwrapReceipt, unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + 0n, // Try to unwrap 0 + signers.alice + ); + + // CHECK: UnwrapStarted event shows success (transfer of 0 succeeds) + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + expect(unwrapStartedEvents[0].args[0]).to.equal(true, "returnVal should be true"); + + // CHECK: UnwrapFinalized event shows failure path (actualBurnAmount is 0) + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + const event = unwrapFinalizedEvents[0]; + expect(event.args[1]).to.equal(false, "ethUnwrapSuccess should be false (failure path)"); + expect(event.args[2]).to.equal(false, "ethFeeSuccess should be false (failure path)"); + expect(event.args[3]).to.equal(0n, "actualBurnAmount should be 0"); + expect(event.args[4]).to.equal(0n, "unwrapAmount should be 0"); + expect(event.args[5]).to.equal(0n, "feeAmount should be 0"); + + // CHECK: All balances unchanged + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcAfter = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcAfter = await usdc.balanceOf(wrapperAddress); + const aliceCTokenAfter = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + expect(aliceUsdcAfter).to.equal(aliceUsdcBefore, "Alice USDC should be unchanged"); + expect(royaltiesUsdcAfter).to.equal(royaltiesUsdcBefore, "Royalties USDC should be unchanged"); + expect(wrapperUsdcAfter).to.equal(wrapperUsdcBefore, "Wrapper USDC should be unchanged"); + expect(aliceCTokenAfter).to.equal(aliceCTokenBefore, "Alice cToken should be unchanged"); + + // VERIFY: Wrapper backing invariant maintained + await verifyWrapperBacking(wrapper); + }); + + it("should revert unwrap to zero address", async function () { + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + + // Try to unwrap to zero address + await expect( + unwrapToken( + wrapper, + ethers.ZeroAddress, + aliceCEthBalance, + signers.alice + ) + ).to.be.revertedWithCustomError(wrapper, "CannotSendToZeroAddress"); + }); + + it("should handle minimum unwrap amount", async function () { + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const rate = await cToken.rate(); + + // Wrap minimum amount + const minWrapAmount = rate * BigInt(10); // Enough for at least 1 token after fees + await wrapERC20(coordinator, usdc, minWrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // CHECK: requestId before unwrap + const requestIdBefore = await wrapper.requestId(); + + // UNWRAP + await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalance, + signers.alice + ); + + // CHECK: requestId was incremented after onConfidentialTransferReceived + const requestIdAfter = await wrapper.requestId(); + expect(requestIdAfter).to.equal(requestIdBefore + 1n); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle partial unwrap", async function () { + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // Unwrap only half + const unwrapAmount = aliceCUsdcBalance / BigInt(2); + + // CHECK: requestId before unwrap + const requestIdBefore = await wrapper.requestId(); + + await unwrapToken( + wrapper, + signers.alice.address, + unwrapAmount, + signers.alice + ); + + // CHECK: requestId was incremented after onConfidentialTransferReceived + const requestIdAfter = await wrapper.requestId(); + expect(requestIdAfter).to.equal(requestIdBefore + 1n); + + // CHECK: alice still has remaining cUSDC + const remainingBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(remainingBalance).to.be.gt(0n); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle multiple unwraps", async function () { + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // CHECK: requestId before unwraps + const requestIdStart = await wrapper.requestId(); + + // Unwrap in 4 parts + for (let i = 0; i < 4; i++) { + const unwrapAmount = aliceCUsdcBalance / BigInt(4); + + const requestIdBefore = await wrapper.requestId(); + + await unwrapToken( + wrapper, + signers.alice.address, + unwrapAmount, + signers.alice + ); + + // CHECK: requestId was incremented after each onConfidentialTransferReceived + const requestIdAfter = await wrapper.requestId(); + expect(requestIdAfter).to.equal(requestIdBefore + 1n); + + // Verify parity after each unwrap + await verifyWrapperBacking(wrapper); + } + + // CHECK: requestId incremented 4 times total + const requestIdEnd = await wrapper.requestId(); + expect(requestIdEnd).to.equal(requestIdStart + 4n); + + // After 4 unwraps, balance should be very small or zero + expect( + await getEncryptedBalance(cToken, signers.alice, cTokenAddress) + ).to.equal(0); + }); + }); + + describe("Fee Calculation", function () { + it("should return correct unwrap fee", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const amount = BigInt(100); + + const adminProviderAddress = await wrapper.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const unwrapFeeBasisPoints = await feeManager.unwrapFeeBasisPoints(); + const unwrapFee = await feeManager.getUnwrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + const expectedUnwrapFee = (amount * unwrapFeeBasisPoints) / BigInt(10_000); + + expect(unwrapFee).to.equal(expectedUnwrapFee); + }); + + it("should commit fee basis points at unwrap start and not be affected by fee changes", async function () { + // This test verifies the fix for OZ2-L02: Fee parameters are committed when unwrap starts + // and the committed values are used in finalizeUnwrap, regardless of subsequent fee changes + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Get alice's balance + const aliceCTokenBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + const rate = await cToken.rate(); + + // Get initial unwrap fee basis points + const initialUnwrapFeeBps = await feeManager.unwrapFeeBasisPoints(); + expect(initialUnwrapFeeBps).to.equal(100n, "Initial fee should be 100 bps (1%)"); + + // Calculate expected fee with INITIAL rate + const expectedFeeAmount = (aliceCTokenBalance * initialUnwrapFeeBps) / BigInt(10_000); + + // START unwrap (onConfidentialTransferReceived) - this commits the fee + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCTokenBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + const requestId = unwrapStartedEvent.args[1]; + + // CHANGE FEE PARAMETERS before finalization (this should NOT affect the unwrap) + const newUnwrapFeeBps = BigInt(500); // 5% - much higher! + await feeManager.setUnwrapFeeBasisPoints(newUnwrapFeeBps); + + // Verify fee was actually changed + const currentUnwrapFeeBps = await feeManager.unwrapFeeBasisPoints(); + expect(currentUnwrapFeeBps).to.equal(newUnwrapFeeBps, "Fee should be changed to 500 bps"); + + // Get balances before finalization + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcBefore = await usdc.balanceOf(signers.royalties.address); + + // FINALIZE unwrap - should use COMMITTED fee (100 bps), not current fee (500 bps) + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + const abiEncodedClearBurnResults = publicDecryptResults.abiEncodedClearValues; + const decryptionProof = publicDecryptResults.decryptionProof; + + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + abiEncodedClearBurnResults, + decryptionProof, + ); + const finalizeReceipt = await unwrapFinalizedTx.wait(); + + // Get balances after finalization + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcAfter = await usdc.balanceOf(signers.royalties.address); + + // Calculate actual fee paid + const actualFeeAmount = royaltiesUsdcAfter - royaltiesUsdcBefore; + const actualUnwrapAmount = aliceUsdcAfter - aliceUsdcBefore; + + // VERIFY: Fee was calculated using INITIAL (committed) basis points, not the changed ones + const expectedFeeAmount256 = expectedFeeAmount * rate; + const expectedUnwrapAmount = aliceCTokenBalance * rate - expectedFeeAmount256; + + expect(actualFeeAmount).to.equal(expectedFeeAmount256, + "Fee should be calculated with committed rate (100 bps), not changed rate (500 bps)"); + expect(actualUnwrapAmount).to.equal(expectedUnwrapAmount, + "Unwrap amount should reflect committed fee"); + + // VERIFY: Event shows correct fee (with committed rate) + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + expect(unwrapFinalizedEvents[0].args[5]).to.equal(expectedFeeAmount256, + "Event should show fee with committed basis points"); + + // VERIFY: If fee was calculated with NEW rate (500 bps), it would be 5x higher + const wrongFeeAmount = (aliceCTokenBalance * newUnwrapFeeBps) / BigInt(10_000) * rate; + expect(actualFeeAmount).to.not.equal(wrongFeeAmount, + "Fee should NOT be calculated with the changed rate"); + expect(actualFeeAmount).to.be.lt(wrongFeeAmount, + "Committed fee should be less than what new rate would charge"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + }); + + describe("Security Checks", function () { + it("should prevent finalizeUnwrap replay", async function () { + const { wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // set wrap fees to 0 to simplify accounting + await feeManager.setWrapFeeBasisPoints(0); + + const unwrapAmount = BigInt(100000); + const wrapAmount = unwrapAmount * BigInt(2); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Unwrap half of what was wrapped + const { unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + unwrapAmount, + signers.alice + ); + + const wrapperBalanceBeforeReplay = await usdc.balanceOf(wrapperAddress); + // CHECK: first unwrap was successful + expect(wrapperBalanceBeforeReplay).to.equal(unwrapAmount); + + if (unwrapFinalizedReceipt === null) { + throw Error("unwrapFinalizedReceipt is null"); + } + + const finalizeUnwrapTx = await ethers.provider.getTransaction(unwrapFinalizedReceipt.hash); + + if (finalizeUnwrapTx === null) { + throw Error("unwrapFinalizedReceipt is null"); + } + + const parsedTx = wrapper.interface.parseTransaction({data: finalizeUnwrapTx.data, value: finalizeUnwrapTx.value}); + if (!parsedTx) { + throw Error("Failed to parse transaction"); + } + + // Extract args - need to copy arrays to avoid read-only issues + const requestId = parsedTx.args[0]; + const abiEncodedClearBurnAmounts = parsedTx.args[1]; + const decryptionProof = parsedTx.args[2]; + + // CHECK: replay raises + await expect(wrapper.finalizeUnwrap(requestId, abiEncodedClearBurnAmounts, decryptionProof)) + .to.be.revertedWithCustomError(wrapper, "ERC7984InvalidGatewayRequest") + .withArgs(requestId); + + // CHECK: wrapper underlying balance did not change + expect(await usdc.balanceOf(wrapperAddress)).to.equal(wrapperBalanceBeforeReplay); + + // CHECK: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }) + + it("should reject unwrap from wrong confidential token", async function () { + const usdt = await deployTestERC20Fixture("USDT"); + const usdtMintTx = await usdt.mint(signers.alice, ethers.parseUnits("100000", 6)); + await usdtMintTx.wait(); + + // DEPLOY first confidential token + wrapper + const { cToken: cToken1, cTokenAddress: cToken1Address, wrapper: wrapper1, wrapperAddress: wrapper1Address } = + await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // DEPLOY second confidential token (different wrapper) + const { cToken: cToken2, cTokenAddress: cToken2Address } = + await deployConfidentialToken(coordinator, usdt, signers.alice); + await wrapERC20(coordinator, usdt, wrapAmount, signers.alice.address, signers.alice); + + // Get initial balances + const alice1BalanceBefore = await getEncryptedBalance(cToken1, signers.alice, cToken1Address); + const alice2BalanceBefore = await getEncryptedBalance(cToken2, signers.alice, cToken2Address); + const wrapper1UsdcBefore = await usdc.balanceOf(wrapper1Address); + + // Attempt to unwrap cToken2 to wrapper1 (wrong wrapper) + // This should fail the msg.sender check since msg.sender will be cToken2, not cToken1 + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken2.getAddress(), signers.alice.address) + .add64(42) + .encrypt(); + + const tx = await cToken2.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + wrapper1Address, + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const receipt = await tx.wait(); + + // CHECK: UnwrappedStarted event shows rejection (returnVal = false, requestId = 0) + const unwrapStartedEvents = getUnwrapStartedEvent(receipt); + expect(unwrapStartedEvents.length).to.equal(1); + expect(unwrapStartedEvents[0].args[0]).to.equal(false, "returnVal should be false"); + expect(unwrapStartedEvents[0].args[1]).to.equal(0n, "requestId should be 0"); + + // CHECK: Alice's balances unchanged (transfer was rejected) + const alice1BalanceAfter = await getEncryptedBalance(cToken1, signers.alice, cToken1Address); + const alice2BalanceAfter = await getEncryptedBalance(cToken2, signers.alice, cToken2Address); + expect(alice1BalanceAfter).to.equal(alice1BalanceBefore, "cToken1 balance should be unchanged"); + expect(alice2BalanceAfter).to.equal(alice2BalanceBefore, "cToken2 balance should be unchanged"); + + // CHECK: Wrapper USDC balance unchanged + const wrapper1UsdcAfter = await usdc.balanceOf(wrapper1Address); + expect(wrapper1UsdcAfter).to.equal(wrapper1UsdcBefore, "Wrapper USDC balance should be unchanged"); + + // VERIFY: Wrapper backing invariant maintained for both wrappers + await verifyWrapperBacking(wrapper1); + }); + }); + + describe("Callback Return Value Handling", function () { + it("should return true from finalizeUnwrap when callback returns true", async function () { + // Deploy mock receiver + const WrapperReceiverMockFactory = await ethers.getContractFactory("WrapperReceiverMock"); + const receiverMock = await WrapperReceiverMockFactory.deploy() as WrapperReceiverMock; + await receiverMock.waitForDeployment(); + const receiverMockAddress = await receiverMock.getAddress(); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Fund the mock receiver with some USDC for the test + await usdc.mint(receiverMockAddress, wrapAmount); + + // Get alice's balance + const aliceCTokenBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // Set callback to return true (default, but being explicit) + await receiverMock.setReturnValue(true); + + // START unwrap to the receiver mock + const callbackData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], ["test-data"]); + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [receiverMockAddress, signers.alice.address, callbackData] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCTokenBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + // FINALIZE unwrap - use staticCall to check return value + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + // Use staticCall to check return value without executing + const returnValue = await wrapper.connect(signers.alice).finalizeUnwrap.staticCall( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // CHECK: finalizeUnwrap should return true + expect(returnValue).to.equal(true, "finalizeUnwrap should return true when callback returns true"); + + // Now actually execute the transaction + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + await unwrapFinalizedTx.wait(); + + // CHECK: callback was called with expected params + expect(await receiverMock.callbackCount()).to.equal(1n); + expect(await receiverMock.lastUnwrapRequestId()).to.equal(unwrapStartedEvent.args.requestId); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should return false from finalizeUnwrap when callback returns false", async function () { + // Deploy mock receiver + const WrapperReceiverMockFactory = await ethers.getContractFactory("WrapperReceiverMock"); + const receiverMock = await WrapperReceiverMockFactory.deploy() as WrapperReceiverMock; + await receiverMock.waitForDeployment(); + const receiverMockAddress = await receiverMock.getAddress(); + + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Fund the mock receiver with some USDC for the test + await usdc.mint(receiverMockAddress, wrapAmount); + + // Get alice's balance + const aliceCTokenBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // Set callback to return false + await receiverMock.setReturnValue(false); + + // START unwrap to the receiver mock + const callbackData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], ["test-data"]); + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [receiverMockAddress, signers.alice.address, callbackData] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCTokenBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + // FINALIZE unwrap - use staticCall to check return value + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + // Use staticCall to check return value without executing + const returnValue = await wrapper.connect(signers.alice).finalizeUnwrap.staticCall( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // CHECK: finalizeUnwrap should return false + expect(returnValue).to.equal(false, "finalizeUnwrap should return false when callback returns false"); + + // Now actually execute the transaction + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + await unwrapFinalizedTx.wait(); + + // CHECK: callback was called + expect(await receiverMock.callbackCount()).to.equal(1n); + expect(await receiverMock.lastUnwrapRequestId()).to.equal(unwrapStartedEvent.args.requestId); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should return true from finalizeUnwrap when recipient is not a contract", async function () { + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Get alice's balance + const aliceCTokenBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // START unwrap to EOA (alice) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCTokenBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + // FINALIZE unwrap - use staticCall to check return value + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + // Use staticCall to check return value without executing + const returnValue = await wrapper.connect(signers.alice).finalizeUnwrap.staticCall( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // CHECK: should return true when no callback is made (EOA recipient) + expect(returnValue).to.equal(true, "finalizeUnwrap should return true for EOA recipient"); + + // Now actually execute the transaction + await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should return false from finalizeUnwrap when unwrap fails", async function () { + // DEPLOY & WRAP + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(1); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Get alice's balance + const aliceCTokenBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // START unwrap with 0 amount (will fail) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + // Unwrap more than balance + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(10) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + expect(unwrapStartedEvents.length).to.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + // FINALIZE unwrap - use staticCall to check return value + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + // Use staticCall to check return value without executing + const returnValue = await wrapper.connect(signers.alice).finalizeUnwrap.staticCall( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // CHECK: should return false when unwrap fails + expect(returnValue).to.equal(false, "finalizeUnwrap should return false when unwrap fails"); + + // Now actually execute the transaction + const unwrapFinalizedTx = await wrapper.connect(signers.alice).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + const finalizeReceipt = await unwrapFinalizedTx.wait(); + + // CHECK: UnwrapFinalized event shows failure + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + expect(unwrapFinalizedEvents[0].args[1]).to.equal(false, "unwrap should fail"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + }); + + describe("Finalize Unwrap Permissions", function () { + it("should allow unwrap initiator to finalize their own unwrap", async function () { + // This test validates existing behavior still works - initiator can finalize + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // Alice initiates and finalizes unwrap (standard flow) + const { unwrapFinalizedReceipt } = await unwrapToken( + wrapper, + signers.alice.address, + aliceCUsdcBalance, + signers.alice + ); + + // CHECK: Unwrap succeeded + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(unwrapFinalizedReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + expect(unwrapFinalizedEvents[0].args[1]).to.equal(true, "unwrap should succeed"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should reject finalization by unauthorized address", async function () { + // Alice initiates unwrap, Bob (unauthorized) tries to finalize + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // START unwrap (alice initiates) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCUsdcBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId from UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const requestId = unwrapStartedEvents[0].args[1]; + + // Get decryption proofs + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvents[0].args[5], + unwrapStartedEvents[0].args[6], + ]); + + // TRY to finalize with bob (unauthorized) + await expect( + wrapper.connect(signers.bob).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ) + ).to.be.revertedWithCustomError(wrapper, "UnauthorizedFinalizeUnwrapCaller") + .withArgs(requestId, signers.bob.address, signers.alice.address); + + // Now alice (authorized) finalizes to complete the unwrap and maintain backing invariant + await wrapper.connect(signers.alice).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // VERIFY: Wrapper backing invariant (after finalization) + await verifyWrapperBacking(wrapper); + }); + + it("should allow authorized operator to finalize unwrap", async function () { + // Alice sets bob as operator, then bob finalizes alice's unwrap + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Alice sets bob as operator (valid until far future) + const futureTimestamp = Math.floor(Date.now() / 1000) + 86400; // +24 hours + const setOperatorTx = await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.bob.address, + futureTimestamp + ); + const setOperatorReceipt = await setOperatorTx.wait(); + + // CHECK: FinalizeUnwrapOperatorSet event + const iface = new ethers.Interface(["event FinalizeUnwrapOperatorSet(address indexed holder, address indexed operator, uint48 until)"]); + const events = setOperatorReceipt?.logs + .map(log => { + try { + return iface.parseLog({ topics: log.topics as string[], data: log.data }); + } catch { + return null; + } + }) + .filter(e => e !== null); + expect(events?.length).to.equal(1); + expect(events?.[0]?.args[0]).to.equal(signers.alice.address); + expect(events?.[0]?.args[1]).to.equal(signers.bob.address); + expect(events?.[0]?.args[2]).to.equal(futureTimestamp); + + // CHECK: isFinalizeUnwrapOperator returns true + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(true); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // START unwrap (alice initiates) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCUsdcBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId and decryption proofs + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const requestId = unwrapStartedEvents[0].args[1]; + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvents[0].args[5], + unwrapStartedEvents[0].args[6], + ]); + + // Bob (authorized operator) finalizes alice's unwrap - should succeed + const finalizeReceipt = await (await wrapper.connect(signers.bob).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + )).wait(); + + // CHECK: Unwrap succeeded + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + expect(unwrapFinalizedEvents[0].args[1]).to.equal(true, "unwrap should succeed"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should reject finalization by expired operator", async function () { + // Alice sets bob as operator with past expiration, bob tries to finalize + const { cToken, cTokenAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const wrapAmount = BigInt(100000); + await wrapERC20(coordinator, usdc, wrapAmount, signers.alice.address, signers.alice); + + // Alice sets bob as operator (already expired) + const pastTimestamp = Math.floor(Date.now() / 1000) - 86400; // -24 hours + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.bob.address, + pastTimestamp + ); + + // CHECK: isFinalizeUnwrapOperator returns false + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(false); + + const aliceCUsdcBalance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + + // START unwrap (alice initiates) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cTokenAddress, signers.alice.address) + .add64(aliceCUsdcBalance) + .encrypt(); + + const unwrapTx = await cToken.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId and decryption proofs + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const requestId = unwrapStartedEvents[0].args[1]; + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvents[0].args[5], + unwrapStartedEvents[0].args[6], + ]); + + // Bob (expired operator) tries to finalize - should fail + await expect( + wrapper.connect(signers.bob).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ) + ).to.be.revertedWithCustomError(wrapper, "UnauthorizedFinalizeUnwrapCaller") + .withArgs(requestId, signers.bob.address, signers.alice.address); + + // Now alice (authorized) finalizes to complete the unwrap and maintain backing invariant + await wrapper.connect(signers.alice).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + ); + + // VERIFY: Wrapper backing invariant (after finalization) + await verifyWrapperBacking(wrapper); + }); + + it("should allow operator to be updated and revoked", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Set bob as operator + const futureTimestamp1 = Math.floor(Date.now() / 1000) + 86400; + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.bob.address, + futureTimestamp1 + ); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(true); + + // Update bob's expiration (extend) + const futureTimestamp2 = Math.floor(Date.now() / 1000) + 172800; // +48 hours + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.bob.address, + futureTimestamp2 + ); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(true); + + // Revoke bob immediately (set to 0) + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.bob.address, + 0 + ); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(false); + + // Set multiple operators + const futureTimestamp3 = Math.floor(Date.now() / 1000) + 86400; + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator(signers.bob.address, futureTimestamp3); + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator(signers.royalties.address, futureTimestamp3); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.bob.address)).to.equal(true); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.royalties.address)).to.equal(true); + }); + + it("should handle holder setting themselves as operator (redundant but allowed)", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Alice can set herself as operator (redundant) + const futureTimestamp = Math.floor(Date.now() / 1000) + 86400; + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator( + signers.alice.address, + futureTimestamp + ); + + // isFinalizeUnwrapOperator returns true (because holder == operator) + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.alice.address)).to.equal(true); + + // Even with timestamp 0, holder can always finalize their own + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator(signers.alice.address, 0); + expect(await wrapper.isFinalizeUnwrapOperator(signers.alice.address, signers.alice.address)).to.equal(true); + }); + + it("should handle ETH unwrap with operator finalization", async function () { + // Test operator can finalize ETH unwraps too + const { cEth, cEthAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + const wrapAmount = ethers.parseEther("1.0"); + await wrapETH(coordinator, wrapAmount, signers.alice.address, signers.alice); + + // Alice sets bob as operator + const futureTimestamp = Math.floor(Date.now() / 1000) + 86400; + await wrapper.connect(signers.alice).setFinalizeUnwrapOperator(signers.bob.address, futureTimestamp); + + const aliceCEthBalance = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + + // START unwrap (alice initiates) + const data = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "bytes"], + [signers.alice.address, signers.alice.address, "0x"] + ); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(cEthAddress, signers.alice.address) + .add64(aliceCEthBalance) + .encrypt(); + + const unwrapTx = await cEth.connect(signers.alice)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + // Get requestId and decryption proofs + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const requestId = unwrapStartedEvents[0].args[1]; + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvents[0].args[5], + unwrapStartedEvents[0].args[6], + ]); + + // Bob finalizes alice's ETH unwrap + const finalizeReceipt = await (await wrapper.connect(signers.bob).finalizeUnwrap( + requestId, + publicDecryptResults.abiEncodedClearValues, + publicDecryptResults.decryptionProof, + )).wait(); + + // CHECK: Unwrap succeeded + const unwrapFinalizedEvents = getUnwrapFinalizedEvent(finalizeReceipt); + expect(unwrapFinalizedEvents.length).to.equal(1); + expect(unwrapFinalizedEvents[0].args[1]).to.equal(true, "unwrap should succeed"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/Wrapper.wrap.ts b/protocol-contracts/confidential-wrapper/test/Wrapper.wrap.ts new file mode 100644 index 0000000000..a9328fcfd3 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/Wrapper.wrap.ts @@ -0,0 +1,1015 @@ +import { ethers, fhevm } from "hardhat"; +import { expect } from "chai"; +import { + getMintEvent, + deployConfidentialToken, + deployConfidentialETH, + getEncryptedBalance, + getWrappedEvent, + getERC20TransferEvent, + wrapETH, + wrapERC20, + getWrapFeeBasisPoints, + verifyWrapperBacking, +} from "./utils"; +import { getSigners, Signers } from "./signers"; +import type { TestERC20, AdminProvider, DeploymentCoordinator, WrapperFactory, RegulatedERC7984UpgradeableFactory, FeeOnTransferERC20, FeeManager, ERC20FailOnAddressMock } from "../types"; +import { deployWrapperFixture, deployTestERC20Fixture, deploySanctionsListFixture, deployTestUnsafeERC20Fixture } from "./fixtures"; +import { FhevmType } from "@fhevm/hardhat-plugin"; + +async function deployFixture(signers: Signers) { + const { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory } = await deployWrapperFixture(signers); + + const usdc = await deployTestERC20Fixture("USDC"); + const usdcAddress = await usdc.getAddress(); + + let transaction = await usdc.mint(signers.alice, ethers.parseUnits("10000", 6)); + await transaction.wait(); + + return { coordinator, coordinatorAddress, adminProvider, confidentialTokenFactory, wrapperFactory, usdc, usdcAddress }; +} + +async function deployFeeOnTransferToken(signers: Signers, transferFeeBasisPoints: number = 100) { + const FeeOnTransferERC20Factory = await ethers.getContractFactory("FeeOnTransferERC20"); + const feeToken = await FeeOnTransferERC20Factory.deploy("FeeToken", "FEE", 6, transferFeeBasisPoints); + await feeToken.waitForDeployment(); + + // Mint to alice + const transaction = await feeToken.mint(signers.alice, ethers.parseUnits("10000", 6)); + await transaction.wait(); + + return feeToken; +} + +describe("Wrapper Fee Handling", function () { + let signers: Signers; + let coordinator: DeploymentCoordinator; + let coordinatorAddress: string; + let adminProvider: AdminProvider; + let confidentialTokenFactory: RegulatedERC7984UpgradeableFactory; + let wrapperFactory: WrapperFactory; + let usdc: TestERC20; + let usdcAddress: string; + let feeManager: FeeManager; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + ({ + coordinator, + coordinatorAddress, + adminProvider, + confidentialTokenFactory, + wrapperFactory, + usdc, + usdcAddress, + } = await deployFixture(signers)); + + const feeManagerAddress = await adminProvider.feeManager(); + feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + }); + + describe("ETH Wrapping with Fees", function () { + it("should revert when fee recipient rejects ETH", async function () { + // Deploy cETH first with normal fee recipient + const { wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + // Deploy RejectEth contract + const RejectEthContract = await ethers.getContractFactory("RejectEth"); + const rejectEth = await RejectEthContract.deploy(); + await rejectEth.waitForDeployment(); + + // Now set RejectEth as fee recipient AFTER deployment + await feeManager.setFeeRecipient(await rejectEth.getAddress()); + + // Try to wrap ETH - should fail because fee recipient rejects ETH + const amount = ethers.parseEther("1.0"); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: amount }) + ).to.be.revertedWithCustomError(wrapper, "EthFeeTransferFailed"); + }); + + it("should wrap ETH with fee - comprehensive balance checks", async function () { + // DEPLOY: cETH + const { cEth, cEthAddress, wrapperAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + // Get initial balances + const aliceEthBefore = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + + // WRAP: ETH -> cETH + const amount = ethers.parseEther("1.0"); + const rate = await cEth.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Calculate expected values based on contract logic + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + const expectedMintAmount = transferAmount / rate; + + // Verify accounting + expect(amount).to.equal(transferAmount + totalFee); + + const wrapTx = await wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: amount }); + const wrapReceipt = await wrapTx.wait(); + + // CHECK: alice cETH balance + const balanceAlice = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + expect(balanceAlice).to.equal(expectedMintAmount, "Alice should receive exactly expectedMintAmount"); + + // CHECK: royalties received NO cETH (fees are in ETH now) + await expect( + getEncryptedBalance(cEth, signers.royalties, cEthAddress), + ).to.be.rejectedWith("Handle is not initialized"); + + // CHECK: ETH balances + const aliceEthAfter = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + + // Alice paid amount + gas + const gasUsed = (wrapReceipt?.gasUsed ?? 0n) * (wrapReceipt?.gasPrice ?? 0n); + expect(aliceEthBefore - aliceEthAfter).to.be.closeTo(amount + gasUsed, ethers.parseEther("0.001")); + + // Royalties received totalFee in ETH + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(totalFee, "Royalties should receive totalFee in ETH"); + + // Wrapper received transferAmount in ETH + expect(wrapperEthAfter - wrapperEthBefore).to.equal(transferAmount, "Wrapper should hold transferAmount in ETH"); + + // CHECK: Mint events + const mintEvents = getMintEvent(wrapReceipt); + expect(mintEvents.length).to.equal(1, "Should have 1 mint event (no fee mint in cETH)"); + expect(mintEvents[0].args[1]).to.equal(expectedMintAmount); + + // CHECK: Wrapped event (for ETH, actualFeeReceived = totalFee) + const wrappedEvents = getWrappedEvent(wrapReceipt); + + expect(wrappedEvents.length).to.equal(1); + expect(wrappedEvents[0]?.args[0]).to.equal(expectedMintAmount, "Wrapped event mintAmount"); + expect(wrappedEvents[0]?.args[1]).to.equal(amount, "Wrapped event amountIn"); + expect(wrappedEvents[0]?.args[2]).to.equal(totalFee, "Wrapped event feeAmount (ETH)"); + expect(wrappedEvents[0]?.args[3]).to.equal(signers.alice.address, "Wrapped event recipient"); + + // VERIFY: Total accounting + const totalEthIn = amount; + const totalEthOut = transferAmount + totalFee; + expect(totalEthIn).to.equal(totalEthOut, "Total ETH in should equal total ETH out"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should wrap ETH without fee - comprehensive balance checks", async function () { + // SETTINGS: remove wrap fee + await feeManager.setWrapFeeBasisPoints(0); + + // DEPLOY: cETH + const { cEth, cEthAddress, wrapperAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + // Get initial balances + const aliceEthBefore = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + + // WRAP: ETH -> cETH + const amount = ethers.parseEther("1.0"); + const rate = await cEth.rate(); + + // Calculate expected values (no base fee) + const baseFee = BigInt(0); + const baseAmount = amount; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = wrapDust; // Only dust, no baseFee + const expectedMintAmount = transferAmount / rate; + + const wrapTx = await wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: amount }); + const wrapReceipt = await wrapTx.wait(); + + // CHECK: alice cETH balance + const balanceAlice = await getEncryptedBalance(cEth, signers.alice, cEthAddress); + expect(balanceAlice).to.equal(expectedMintAmount); + + // CHECK: royalties received NO cETH + await expect( + getEncryptedBalance(cEth, signers.royalties, cEthAddress), + ).to.be.rejectedWith("Handle is not initialized"); + + // CHECK: ETH balances + const aliceEthAfter = await ethers.provider.getBalance(signers.alice.address); + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + + // Royalties received only dust in ETH + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(totalFee); + + // Wrapper received transferAmount in ETH + expect(wrapperEthAfter - wrapperEthBefore).to.equal(transferAmount); + + // VERIFY: Total accounting + expect(amount).to.equal(transferAmount + totalFee); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle dust correctly for various amounts", async function () { + const { cEth, cEthAddress, wrapper, wrapperAddress } = await deployConfidentialETH(coordinator, signers.alice); + const rate = await cEth.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Test various amounts with different dust values + const testAmounts = [ + ethers.parseEther("0.1"), + ethers.parseEther("0.123456789"), + ethers.parseEther("1.0"), + ethers.parseEther("1.999999999"), + ]; + + for (const amount of testAmounts) { + const royaltiesEthBefore = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthBefore = await ethers.provider.getBalance(wrapperAddress); + + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + const expectedMintAmount = transferAmount / rate; + + await wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: amount }); + + const royaltiesEthAfter = await ethers.provider.getBalance(signers.royalties.address); + const wrapperEthAfter = await ethers.provider.getBalance(wrapperAddress); + + expect(royaltiesEthAfter - royaltiesEthBefore).to.equal(totalFee, `Royalties should receive totalFee for amount ${amount}`); + expect(wrapperEthAfter - wrapperEthBefore).to.equal(transferAmount, `Wrapper should hold transferAmount for amount ${amount}`); + expect(amount).to.equal(transferAmount + totalFee, `Accounting should balance for amount ${amount}`); + } + }); + }); + + describe("ERC20 Wrapping with Fees", function () { + it("should wrap USDC with fee - comprehensive balance checks", async function () { + // DEPLOY: cUSDC + const { cToken, cTokenAddress, wrapperAddress, wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + // Get initial balances + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcBefore = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcBefore = await usdc.balanceOf(wrapperAddress); + + // WRAP: USDC -> cUSDC + const amount = BigInt(1000); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Calculate expected values + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + const expectedMintAmount = transferAmount / rate; + + // Approve full amount (contract needs to transfer transferAmount + totalFee) + await usdc.connect(signers.alice).approve(wrapperAddress, amount); + + const wrapTx = await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + const wrapReceipt = await wrapTx.wait(); + + // CHECK: alice cUSDC balance + const balanceAlice = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(balanceAlice).to.equal(expectedMintAmount, "Alice should receive exactly expectedMintAmount"); + + // CHECK: royalties received NO cUSDC (fees are in USDC now) + await expect( + getEncryptedBalance(cToken, signers.royalties, cTokenAddress), + ).to.be.rejectedWith("Handle is not initialized"); + + // CHECK: USDC balances + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + const royaltiesUsdcAfter = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcAfter = await usdc.balanceOf(wrapperAddress); + + // Alice paid full amount + expect(aliceUsdcBefore - aliceUsdcAfter).to.equal(amount, "Alice should pay full amount"); + + // Royalties received totalFee in USDC + expect(royaltiesUsdcAfter - royaltiesUsdcBefore).to.equal(totalFee, "Royalties should receive totalFee in USDC"); + + // Wrapper received transferAmount in USDC + expect(wrapperUsdcAfter - wrapperUsdcBefore).to.equal(transferAmount, "Wrapper should hold transferAmount in USDC"); + + // VERIFY: Total accounting + expect(amount).to.equal(transferAmount + totalFee, "Total USDC should balance"); + + // CHECK: No more and no less tokens + const totalUsdcMoved = (aliceUsdcBefore - aliceUsdcAfter); + const totalUsdcReceived = (royaltiesUsdcAfter - royaltiesUsdcBefore) + (wrapperUsdcAfter - wrapperUsdcBefore); + expect(totalUsdcMoved).to.equal(totalUsdcReceived, "All USDC should be accounted for"); + + // CHECK: Mint events + const mintEvents = getMintEvent(wrapReceipt); + expect(mintEvents.length).to.equal(1, "Should have 1 mint event"); + expect(mintEvents[0].args[1]).to.equal(expectedMintAmount); + + // CHECK: Wrapped event (for standard ERC20, actualFeeReceived = totalFee) + const wrappedEvents = getWrappedEvent(wrapReceipt); + expect(wrappedEvents.length).to.equal(1); + expect(wrappedEvents[0]?.args[0]).to.equal(expectedMintAmount, "Wrapped event mintAmount"); + expect(wrappedEvents[0]?.args[1]).to.equal(amount, "Wrapped event amountIn"); + expect(wrappedEvents[0]?.args[2]).to.equal(totalFee, "Wrapped event feeAmount (standard ERC20)"); + expect(wrappedEvents[0]?.args[3]).to.equal(signers.alice.address, "Wrapped event recipient"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should handle dust correctly for various amounts", async function () { + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Test various amounts with different dust values + const testAmounts = [ + BigInt(100), + BigInt(999), + BigInt(1000), + BigInt(1234567), + BigInt(9999999), + ]; + + for (const amount of testAmounts) { + const royaltiesUsdcBefore = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcBefore = await usdc.balanceOf(wrapperAddress); + const aliceUsdcBefore = await usdc.balanceOf(signers.alice.address); + + const baseFee = (amount * wrapFeeBasisPoints + 10_000n - 1n) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + + await usdc.connect(signers.alice).approve(wrapperAddress, amount); + await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + + const royaltiesUsdcAfter = await usdc.balanceOf(signers.royalties.address); + const wrapperUsdcAfter = await usdc.balanceOf(wrapperAddress); + const aliceUsdcAfter = await usdc.balanceOf(signers.alice.address); + + expect(royaltiesUsdcAfter - royaltiesUsdcBefore).to.equal(totalFee, `Royalties fee for ${amount}`); + expect(wrapperUsdcAfter - wrapperUsdcBefore).to.equal(transferAmount, `Wrapper balance for ${amount}`); + expect(aliceUsdcBefore - aliceUsdcAfter).to.equal(amount, `Alice payment for ${amount}`); + expect(amount).to.equal(transferAmount + totalFee, `Accounting for ${amount}`); + } + }); + }); + + it("should use safeTransferFrom for principal transfer", async function () { + const unsafeUsdc = await deployTestUnsafeERC20Fixture("ERC20ReturnFalseMock", "Unsafe USDC", 6) + const mintTransaction = await unsafeUsdc.mint(signers.alice, 2_000); + await mintTransaction.wait(); + + const { wrapper } = await deployConfidentialToken(coordinator, unsafeUsdc, signers.alice); + + const amount = BigInt(100); + + await unsafeUsdc.connect(signers.alice).approve(await wrapper.getAddress(), amount); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount) + ).to.be.revertedWithCustomError(wrapper, "SafeERC20FailedOperation"); + }); + + it("should use safeTransferFrom for fee transfer", async function () { + // Deploy mock that fails on transfers to fee recipient + const unsafeUsdcAddress = await (await deployTestUnsafeERC20Fixture("ERC20FailOnAddressMock", "Unsafe USDC", 6)).getAddress(); + const unsafeUsdc = (await ethers.getContractAt("ERC20FailOnAddressMock", unsafeUsdcAddress)) as ERC20FailOnAddressMock; + const mintTransaction = await unsafeUsdc.mint(signers.alice, 2_000); + await mintTransaction.wait(); + + const { wrapper } = await deployConfidentialToken(coordinator, unsafeUsdc, signers.alice); + + // Configure mock to fail on transfers to fee recipient + const feeRecipient = await feeManager.getFeeRecipient(); + await unsafeUsdc.setFailOnTransferTo(feeRecipient, true); + + const amount = BigInt(100); + + await unsafeUsdc.connect(signers.alice).approve(await wrapper.getAddress(), amount); + + // This should fail on the second safeTransferFrom (fee transfer to fee recipient) + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount) + ).to.be.revertedWithCustomError(wrapper, "SafeERC20FailedOperation"); + }); + + describe("Fee-on-Transfer Token Support", function () { + it("should handle fee-on-transfer tokens and transferDust correctly", async function () { + const transferFeeBasisPoints = 100; // 1% transfer fee + const feeToken = await deployFeeOnTransferToken(signers, transferFeeBasisPoints); + const feeTokenAddress = await feeToken.getAddress(); + + // DEPLOY: cFEE + const { cToken, cTokenAddress, wrapperAddress, wrapper } = await deployConfidentialToken(coordinator, feeToken as any, signers.alice); + + // Get initial balances + const aliceBefore = await feeToken.balanceOf(signers.alice.address); + const royaltiesBefore = await feeToken.balanceOf(signers.royalties.address); + const wrapperBefore = await feeToken.balanceOf(wrapperAddress); + const feeCollectorBefore = await feeToken.balanceOf(await feeToken.feeCollector()); + + // WRAP + const amount = BigInt(10000); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Calculate expected values BEFORE fee-on-transfer + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + + // Calculate actual received after fee-on-transfer + const transferFee = (transferAmount * BigInt(transferFeeBasisPoints)) / BigInt(10_000); + const actualReceived = transferAmount - transferFee; + const transferDust = actualReceived % rate; + const expectedMintAmount = (actualReceived - transferDust) / rate; + + // Approve full amount + await feeToken.connect(signers.alice).approve(wrapperAddress, amount); + + const wrapTx = await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + const wrapReceipt = await wrapTx.wait(); + + // CHECK: balances after + const aliceAfter = await feeToken.balanceOf(signers.alice.address); + const royaltiesAfter = await feeToken.balanceOf(signers.royalties.address); + const wrapperAfter = await feeToken.balanceOf(wrapperAddress); + const feeCollectorAfter = await feeToken.balanceOf(await feeToken.feeCollector()); + + // Alice paid full amount + expect(aliceBefore - aliceAfter).to.equal(amount, "Alice should pay full amount"); + + // Wrapper received less due to fee-on-transfer, minus dust sent back + expect(wrapperAfter - wrapperBefore).to.equal(actualReceived - transferDust, "Wrapper balance after dust transfer"); + + // Royalties received (baseFee + wrapDust) minus the fee-on-transfer, plus transferDust + // Note: The totalFee transfer from alice to royalties is also subject to fee-on-transfer! + const totalFeeFee = (totalFee * BigInt(transferFeeBasisPoints)) / BigInt(10_000); + const totalFeeReceived = totalFee - totalFeeFee; + const transferDustFee = transferDust > 0 ? (transferDust * BigInt(transferFeeBasisPoints)) / BigInt(10_000) : BigInt(0); + const transferDustReceived = transferDust - transferDustFee; + const royaltiesReceived = royaltiesAfter - royaltiesBefore; + expect(royaltiesReceived).to.equal(totalFeeReceived + transferDustReceived, "Royalties should receive fees and dust after fee-on-transfer"); + + // Fee collector received fees from both transfers (transferAmount and totalFee, plus transferDust if any) + const expectedFeeCollectorTotal = transferFee + totalFeeFee + transferDustFee; + expect(feeCollectorAfter - feeCollectorBefore).to.equal(expectedFeeCollectorTotal, "Fee collector should receive all transfer fees"); + + // Verify alice's cToken balance + const balanceAlice = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(balanceAlice).to.equal(expectedMintAmount, "Alice cToken balance"); + + // VERIFY: Total accounting (considering fee-on-transfer loss) + const totalOut = (wrapperAfter - wrapperBefore) + (royaltiesAfter - royaltiesBefore) + (feeCollectorAfter - feeCollectorBefore); + expect(aliceBefore - aliceAfter).to.equal(totalOut, "All tokens should be accounted for"); + + // CHECK: Mint events + const mintEvents = getMintEvent(wrapReceipt); + expect(mintEvents.length).to.equal(1, "Should have 1 mint event"); + expect(mintEvents[0].args[1]).to.equal(expectedMintAmount); + + // CHECK: Wrapped event (for fee-on-transfer tokens, actualFeeReceived < totalFee) + const wrappedEvents = getWrappedEvent(wrapReceipt); + expect(wrappedEvents.length).to.equal(1); + expect(wrappedEvents[0]?.args[0]).to.equal(expectedMintAmount, "Wrapped event mintAmount"); + expect(wrappedEvents[0]?.args[1]).to.equal(amount, "Wrapped event amountIn"); + // The event should emit the ACTUAL fee received by royalties, not the totalFee sent + expect(wrappedEvents[0]?.args[2]).to.equal(totalFeeReceived + transferDustReceived, "Wrapped event feeAmount (fee-on-transfer)"); + expect(wrappedEvents[0]?.args[3]).to.equal(signers.alice.address, "Wrapped event recipient"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("should emit Wrapped event with fee amount including transferDust (OZ2-L01 regression test)", async function () { + // Use 18 decimals (like ETH/DAI) so rate = 10^12, which will produce transferDust + // Use 1.37% transfer fee (prime-ish) to create remainder when dividing by rate + const transferFeeBasisPoints = 137; + const FeeOnTransferERC20Factory = await ethers.getContractFactory("FeeOnTransferERC20"); + const feeToken = await FeeOnTransferERC20Factory.deploy("FeeToken", "FEE", 18, transferFeeBasisPoints); + await feeToken.waitForDeployment(); + await feeToken.mint(signers.alice, ethers.parseUnits("10000", 18)); + const feeTokenAddress = await feeToken.getAddress(); + + const { cToken, cTokenAddress, wrapperAddress, wrapper } = await deployConfidentialToken(coordinator, feeToken as any, signers.alice); + + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Choose non-round amount that will produce transferDust with fee-on-transfer + // With 18 decimals and 1.37% transfer fee, this creates transferDust + const amount = ethers.parseUnits("1.001", 18); + + // Calculate protocol fee and dust + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + const totalFee = baseFee + wrapDust; + + // Calculate actual amounts after fee-on-transfer deductions + const transferFee = (transferAmount * BigInt(transferFeeBasisPoints)) / BigInt(10_000); + const actualReceived = transferAmount - transferFee; + const transferDust = actualReceived % rate; + + // Calculate what the fee recipient actually receives + const totalFeeFee = (totalFee * BigInt(transferFeeBasisPoints)) / BigInt(10_000); + const totalFeeReceived = totalFee - totalFeeFee; + const transferDustFee = transferDust > 0 ? (transferDust * BigInt(transferFeeBasisPoints)) / BigInt(10_000) : BigInt(0); + const transferDustReceived = transferDust - transferDustFee; + + // Track fee recipient balance to verify emitted amount matches actual received + const royaltiesBefore = await feeToken.balanceOf(signers.royalties.address); + + await feeToken.connect(signers.alice).approve(wrapperAddress, amount); + const wrapTx = await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + const wrapReceipt = await wrapTx.wait(); + + const royaltiesAfter = await feeToken.balanceOf(signers.royalties.address); + const actualFeeRecipientReceived = royaltiesAfter - royaltiesBefore; + + // Verify transferDust exists (required for this test to be meaningful) + expect(transferDust).to.be.greaterThan(0, "transferDust must be non-zero for this test"); + + // CRITICAL: Verify Wrapped event emits the ACTUAL fee received (including dust) + const wrappedEvents = getWrappedEvent(wrapReceipt); + expect(wrappedEvents.length).to.equal(1); + + const emittedFeeAmount = wrappedEvents[0]?.args[2]; + const expectedFeeAmount = totalFeeReceived + transferDustReceived; + + expect(emittedFeeAmount).to.equal(expectedFeeAmount, + "Wrapped event feeAmount must include both protocol fee and transferDust"); + + expect(emittedFeeAmount).to.equal(actualFeeRecipientReceived, + "Wrapped event feeAmount must match actual amount received by fee recipient"); + + // Verify the fix: emittedFeeAmount should be GREATER than just the protocol fee + expect(emittedFeeAmount).to.be.greaterThan(totalFeeReceived, + "Emitted fee must include transferDust (old bug would fail this check)"); + + // VERIFY: Event order (OZ2-L01 also mentioned transfer order changed) + // The fix moves feeBalBefore tracking before both transfers, ensuring correct order: + // 1. Transfer from user to wrapper (for principal) + // 2. Transfer from user to feeRecipient (for fees) + // 3. Transfer from wrapper to feeRecipient (for dust) + const transferEvents = getERC20TransferEvent(wrapReceipt); + expect(transferEvents.length).to.be.greaterThanOrEqual(3, "Should have at least 3 Transfer events"); + + // Find the fee transfer and dust transfer (both to fee recipient) + const toFeeRecipient = transferEvents.filter((e: any) => e.args.to === signers.royalties.address); + expect(toFeeRecipient.length).to.equal(2, "Should have exactly 2 transfers to fee recipient"); + + // First transfer to fee recipient: from user (totalFee) + expect(toFeeRecipient[0].args.from).to.equal(signers.alice.address, "First fee transfer from user"); + + // Second transfer to fee recipient: from wrapper (transferDust) + expect(toFeeRecipient[1].args.from).to.equal(wrapperAddress, "Second fee transfer (dust) from wrapper"); + + // Verify order: user fee transfer should come BEFORE wrapper dust transfer + const feeTransferIndex = transferEvents.indexOf(toFeeRecipient[0]); + const dustTransferIndex = transferEvents.indexOf(toFeeRecipient[1]); + expect(feeTransferIndex).to.be.lessThan(dustTransferIndex, + "Fee transfer must occur before dust transfer (OZ2-L01 fix)"); + }); + + it("should handle multiple fee-on-transfer percentages", async function () { + const testFees = [50, 100, 500, 1000]; // 0.5%, 1%, 5%, 10% + + for (const transferFeeBps of testFees) { + const feeToken = await deployFeeOnTransferToken(signers, transferFeeBps); + const { cToken, cTokenAddress, wrapperAddress, wrapper } = await deployConfidentialToken(coordinator, feeToken as any, signers.alice); + + const amount = BigInt(10000); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + const baseFee = (amount * wrapFeeBasisPoints) / BigInt(10_000); + const baseAmount = amount - baseFee; + const wrapDust = baseAmount % rate; + const transferAmount = baseAmount - wrapDust; + + const transferFee = (transferAmount * BigInt(transferFeeBps)) / BigInt(10_000); + const actualReceived = transferAmount - transferFee; + const transferDust = actualReceived % rate; + const expectedMintAmount = (actualReceived - transferDust) / rate; + + await feeToken.connect(signers.alice).approve(wrapperAddress, amount); + await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + + const balanceAlice = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(balanceAlice).to.equal(expectedMintAmount, `Mint amount for ${transferFeeBps} bps fee`); + } + }); + + it("should verify no tokens are lost in fee-on-transfer scenario", async function () { + const feeToken = await deployFeeOnTransferToken(signers, 100); + const { cToken, cTokenAddress, wrapperAddress, wrapper } = await deployConfidentialToken(coordinator, feeToken as any, signers.alice); + + const totalSupplyBefore = await feeToken.totalSupply(); + const amount = BigInt(10000); + + await feeToken.connect(signers.alice).approve(wrapperAddress, amount); + await wrapper.connect(signers.alice).wrap(signers.alice.address, amount); + + const totalSupplyAfter = await feeToken.totalSupply(); + + // Total supply should not change (no tokens burned/minted) + expect(totalSupplyAfter).to.equal(totalSupplyBefore, "Total supply should remain constant"); + }); + }); + + describe("Edge Cases", function () { + it("should handle minimum wrap amount", async function () { + const { cToken, cTokenAddress, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const rate = await cToken.rate(); + + // Minimum amount that results in at least 1 token after fees and dust + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + const minAmount = rate + (rate * wrapFeeBasisPoints) / BigInt(10_000) + BigInt(1); + + await usdc.connect(signers.alice).approve(wrapperAddress, minAmount); + await wrapper.connect(signers.alice).wrap(signers.alice.address, minAmount); + + const balance = await getEncryptedBalance(cToken, signers.alice, cTokenAddress); + expect(balance).to.be.gte(1n, "Should mint at least 1 token"); + }); + + it("should handle maximum uint64 amount", async function () { + const { cToken, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + const maxUint64 = BigInt("18446744073709551615"); + //const maxAmount = maxUint64 * rate; + + // We want: transferAmount = (maxUint64 + 1) * rate + const targetTransferAmount = (maxUint64) * rate; + + // Working backwards from: transferAmount ≈ amount * (10_000 - wrapFeeBasisPoints) / 10_000 + // (ignoring dust for simplicity, which only makes the issue worse) + // amount = transferAmount * 10_000 / (10_000 - wrapFeeBasisPoints) + const maxAmount = (targetTransferAmount * BigInt(10_000)) / (BigInt(10_000) - wrapFeeBasisPoints); + + // Mint enough tokens + await usdc.mint(signers.alice, maxAmount); + await usdc.connect(signers.alice).approve(wrapperAddress, maxAmount); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, maxAmount) + ).to.not.be.reverted; + }); + + it("should revert on amount too large causing overflow", async function () { + const { cToken, wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const rate = await cToken.rate(); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // We need to calculate an amount such that after fees are taken, + // transferAmount / rate will exceed maxUint64 + const maxUint64 = BigInt("18446744073709551615"); + + // We want: transferAmount = (maxUint64 + 1) * rate + const targetTransferAmount = (maxUint64 + BigInt(1)) * rate; + + // Working backwards from: baseAmount = amount - baseFee + // With ceiling division: baseFee = ceil(amount * wrapFeeBasisPoints / 10_000) + // For worst case, assume: baseFee ≈ (amount * wrapFeeBasisPoints + 9_999) / 10_000 + // So: baseAmount ≈ amount - (amount * wrapFeeBasisPoints + 9_999) / 10_000 + // baseAmount ≈ (amount * 10_000 - amount * wrapFeeBasisPoints - 9_999) / 10_000 + // baseAmount ≈ (amount * (10_000 - wrapFeeBasisPoints) - 9_999) / 10_000 + // Ignoring dust (wrapDust): transferAmount ≈ baseAmount + // We need: transferAmount ≥ targetTransferAmount + // So: (amount * (10_000 - wrapFeeBasisPoints) - 9_999) / 10_000 ≥ targetTransferAmount + // amount * (10_000 - wrapFeeBasisPoints) - 9_999 ≥ targetTransferAmount * 10_000 + // amount ≥ (targetTransferAmount * 10_000 + 9_999) / (10_000 - wrapFeeBasisPoints) + const MAX_BASIS_POINTS = BigInt(10_000); + const overflowAmount = (targetTransferAmount * MAX_BASIS_POINTS + MAX_BASIS_POINTS - BigInt(1)) / (MAX_BASIS_POINTS - wrapFeeBasisPoints); + + await usdc.mint(signers.alice, overflowAmount); + await usdc.connect(signers.alice).approve(wrapperAddress, overflowAmount); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, overflowAmount) + ).to.be.reverted; + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + }); + + describe("Sanctions List", function () { + it("should prevent ETH wrapping when sender is sanctioned", async function () { + const { cEth } = await deployConfidentialETH(coordinator, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + // Sanction alice + await sanctionsList.addToSanctionsList([signers.alice.address]); + + const amount = ethers.parseEther("0.1"); + + // Try to wrap ETH when sender is sanctioned + await expect( + wrapETH(coordinator, amount, signers.alice.address, signers.alice), + ).to.be.revertedWithCustomError(cEth, "SanctionedAddress"); + }); + + it("should prevent ETH wrapping when recipient (to_) is sanctioned", async function () { + const { cEth } = await deployConfidentialETH(coordinator, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + // Sanction bob (the recipient) + await sanctionsList.addToSanctionsList([signers.bob.address]); + + const amount = ethers.parseEther("0.1"); + + // Try to wrap ETH when recipient is sanctioned + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.be.revertedWithCustomError(cEth, "SanctionedAddress"); + }); + + it("should prevent ERC20 wrapping when sender is sanctioned", async function () { + const { cToken } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + // Sanction alice + await sanctionsList.addToSanctionsList([signers.alice.address]); + + const amount = BigInt(100); + + // Try to wrap when sender is sanctioned + await expect( + wrapERC20(coordinator, usdc, amount, signers.alice.address, signers.alice), + ).to.be.revertedWithCustomError(cToken, "SanctionedAddress"); + }); + + it("should prevent ERC20 wrapping when recipient (to_) is sanctioned", async function () { + const { cToken } = await deployConfidentialToken(coordinator, usdc, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + // Sanction bob (the recipient) + await sanctionsList.addToSanctionsList([signers.bob.address]); + + const amount = BigInt(100); + + // Try to wrap when recipient is sanctioned + await expect( + wrapERC20(coordinator, usdc, amount, signers.bob.address, signers.alice), + ).to.be.revertedWithCustomError(cToken, "SanctionedAddress"); + }); + + it("should allow wrapping when no sanctions are in place", async function () { + await deployConfidentialETH(coordinator, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + // Sanction someone else, not involved in the transaction + await sanctionsList.addToSanctionsList([signers.charlie.address]); + + const amount = ethers.parseEther("0.1"); + + // Should work fine when neither sender nor recipient is sanctioned + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.not.be.reverted; + }); + + it("should wrap when wrapper has no sanctions list", async function () { + // Deploy token without setting sanctions list + await deployConfidentialETH(coordinator, signers.alice); + + const amount = ethers.parseEther("0.1"); + + // Should work fine without any sanctions list + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.not.be.reverted; + }); + + it("should allow dynamic sanctions updates affecting wrap operations", async function () { + const { cEth } = await deployConfidentialETH(coordinator, signers.alice); + const { sanctionsList } = await deploySanctionsListFixture(); + await adminProvider.setSanctionsList(sanctionsList); + + const amount = ethers.parseEther("0.1"); + + // Initially should work + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.not.be.reverted; + + // Add bob to sanctions list + await sanctionsList.addToSanctionsList([signers.bob.address]); + + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.be.revertedWithCustomError(cEth, "SanctionedAddress"); + + // Remove bob from sanctions list + await sanctionsList.removeFromSanctionsList([signers.bob.address]); + + // Should work again + await expect( + wrapETH(coordinator, amount, signers.bob.address, signers.alice), + ).to.not.be.reverted; + }); + }); + + describe("Input Validation", function () { + it("should revert on incorrect ETH amount for ETH wrapping", async function () { + const { wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + const amount = ethers.parseEther("1.0"); + const incorrectValue = ethers.parseEther("0.5"); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: incorrectValue }) + ).to.be.revertedWithCustomError(wrapper, "IncorrectEthAmount"); + }); + + it("should revert when sending ETH for token wrapping", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const amount = BigInt(100); + const ethValue = ethers.parseEther("0.1"); + + // Approve first + await usdc.connect(signers.alice).approve(await wrapper.getAddress(), amount); + + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: ethValue }) + ).to.be.revertedWithCustomError(wrapper, "CannotReceiveEthForTokenWrap"); + }); + }); + + describe("Fee Calculation", function () { + it("should return correct wrap fee", async function () { + const { wrapper } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const amount = BigInt(100); + + const wrapFeeBasisPoints = await getWrapFeeBasisPoints(wrapper); + const adminProviderAddress = await wrapper.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFee = await feeManager.getWrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress); + const expectedWrapPrice = (amount * wrapFeeBasisPoints) / BigInt(10_000); + + expect(wrapFee).to.equal(expectedWrapPrice); + }); + }); + + describe("Overflow Protection", function () { + it("ETH should revert when passing boundary: wrapping up to exactly type(uint64).max + 1", async function () { + const { cEth, wrapperAddress, wrapper } = await deployConfidentialETH(coordinator, signers.alice); + + const maxUint64 = 18446744073709551615n; + + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Calculate amount that results in exactly maxUint64 in wrapper (accounting for fees) + const rate = await cEth.rate(); + const amount = ((maxUint64 * 10000n) / (10000n - wrapFeeBasisPoints) + 1n) * rate; + + await ethers.provider.send("hardhat_setBalance", [ + signers.alice.address, + ethers.toBeHex(ethers.parseEther("18446744073709551615")) + ]); + + // This should succeed (at the limit) + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount, { value: amount }) + ).to.not.be.reverted; + + const wrapperBalance = await usdc.balanceOf(wrapperAddress); + expect(wrapperBalance).to.be.lte(maxUint64); + + // But two more units should fail since it'll overflow after fee + const newAmount = 2n * rate + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, newAmount, { value: newAmount }) + ).to.be.revertedWithCustomError(wrapper, "WrapperBalanceExceedsMaxSupply"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("ERC20 should revert when passing boundary: wrapping up to exactly type(uint64).max + 1", async function () { + const { wrapper, wrapperAddress } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const maxUint64 = 18446744073709551615n; + + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFeeBasisPoints = await feeManager.wrapFeeBasisPoints(); + + // Calculate amount that results in exactly maxUint64 in wrapper (accounting for ceiling division fees) + // With ceiling division: baseFee = ceil(amount * wrapFeeBasisPoints / 10000) + // We want: amount - baseFee ≈ maxUint64 + // So: amount - ceil(amount * wrapFeeBasisPoints / 10000) = maxUint64 + // Solving: amount ≈ (maxUint64 * 10000 + 10000 - 1) / (10000 - wrapFeeBasisPoints) + // This accounts for the ceiling division rounding up the fee + const MAX_BASIS_POINTS = 10000n; + const amount = (maxUint64 * MAX_BASIS_POINTS + MAX_BASIS_POINTS - 1n) / (MAX_BASIS_POINTS - wrapFeeBasisPoints); + + await usdc.mint(signers.alice.address, amount + 10000n); + await usdc.connect(signers.alice).approve(wrapperAddress, ethers.MaxUint256); + + // This should succeed (at the limit) + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, amount) + ).to.not.be.reverted; + + const wrapperBalance = await usdc.balanceOf(wrapperAddress); + expect(wrapperBalance).to.be.lte(maxUint64); + + // But wrapping more should fail + // With ceiling division, we need to wrap enough that at least 1 unit reaches the wrapper + // For wrapFeeBasisPoints = 100 (1%): ceil(101 * 100 / 10000) = 2, so 101 - 2 = 99 units reach wrapper + const additionalAmount = 101n; + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, additionalAmount) + ).to.be.revertedWithCustomError(wrapper, "WrapperBalanceExceedsMaxSupply"); + + // VERIFY: Wrapper backing invariant + await verifyWrapperBacking(wrapper); + }); + + it("ETH should not brick wrapper when cTokens are sent directly to wrapper", async function () { + const { wrapper, wrapperAddress, cEth } = await deployConfidentialETH(coordinator, signers.alice); + + await ethers.provider.send("hardhat_setBalance", [ + wrapperAddress, + ethers.toBeHex(ethers.parseEther("18446744073709551615")) + ]); + + expect(await ethers.provider.getBalance(wrapperAddress)).to.equal(ethers.parseEther("18446744073709551615")) + + const rate = await cEth.rate(); + + // Should still be able to wrap more tokens without being bricked + // If the direct transfer bricked the wrapper, this would fail + const wrapAmount = 10000n * rate; + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, wrapAmount, { value: wrapAmount }) + ).to.not.be.reverted; + + // VERIFY: Wrapper total supply matches cToken total supply + const totalSupplyHandle = await cEth.confidentialTotalSupply(); + const totalSupply = await fhevm.publicDecryptEuint(FhevmType.euint64, totalSupplyHandle); + expect(await wrapper.mintedSupply()).to.equal(totalSupply); + }); + + it("ERC20 should not brick wrapper when cTokens are sent directly to wrapper", async function () { + const { wrapper, wrapperAddress, cToken } = await deployConfidentialToken(coordinator, usdc, signers.alice); + + const rate = await cToken.rate(); + + // Mint enough tokens for testing + const maxUint64 = 18446744073709551615n; + await usdc.mint(wrapperAddress, maxUint64 * rate); + await usdc.mint(signers.alice.address, 100000n * rate); + await usdc.connect(signers.alice).approve(wrapperAddress, ethers.MaxUint256); + + const wrapAmount = 10000n * rate; + await expect( + wrapper.connect(signers.alice).wrap(signers.alice.address, wrapAmount) + ).to.not.be.reverted; + + // VERIFY: Wrapper total supply matches cToken total supply + const totalSupplyHandle = await cToken.confidentialTotalSupply(); + const totalSupply = await fhevm.publicDecryptEuint(FhevmType.euint64, totalSupplyHandle); + expect(await wrapper.mintedSupply()).to.equal(totalSupply); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/WrapperFactory.ts b/protocol-contracts/confidential-wrapper/test/WrapperFactory.ts new file mode 100644 index 0000000000..15547521c0 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/WrapperFactory.ts @@ -0,0 +1,253 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { getSigners, Signers } from "./signers"; +import { WrapperFactory, AdminProvider, RegulatedERC7984Upgradeable, WrapperUpgradeable, DeploymentCoordinator } from "../types"; +import { deployWrapperFactoryFixture, deployAdminProviderFixture, deployConfidentialErc20Fixture, deployTestERC20Fixture, } from "./fixtures"; + +describe("WrapperFactory", function () { + let signers: Signers; + let wrapperFactory: WrapperFactory; + let adminProvider: AdminProvider; + let cUsdc: RegulatedERC7984Upgradeable; + let wrapperImplementation: WrapperUpgradeable; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + const { wrapperFactory: deployedFactory } = await deployWrapperFactoryFixture(); + wrapperFactory = deployedFactory; + + const { adminProvider: deployedProvider } = await deployAdminProviderFixture(signers); + adminProvider = deployedProvider; + + ({ cErc20: cUsdc } = await deployConfidentialErc20Fixture(signers)); + + // Deploy WrapperUpgradeable implementation + const wrapperUpgradeableFactory = await ethers.getContractFactory("WrapperUpgradeable"); + wrapperImplementation = await wrapperUpgradeableFactory.deploy() as WrapperUpgradeable; + await wrapperImplementation.waitForDeployment(); + }); + + describe("deployWrapper", function () { + it("should prevent non-owner from calling deployWrapper", async function () { + const originalToken = ethers.ZeroAddress; // ETH wrapper + + await expect( + wrapperFactory.connect(signers.alice).deployWrapper( + await wrapperImplementation.getAddress(), + originalToken, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ) + ).to.be.revertedWithCustomError(wrapperFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.alice.address); + }); + + it("should allow owner to call deployWrapper", async function () { + const originalToken = ethers.ZeroAddress; // ETH wrapper + + const tx = await wrapperFactory.deployWrapper( + await wrapperImplementation.getAddress(), + originalToken, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ); + + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + }); + + it("should prevent non-owner from calling deployWrapper with ERC20 token", async function () { + const testToken = await deployTestERC20Fixture("Test Token", 6); + const testTokenAddress = await testToken.getAddress(); + + await expect( + wrapperFactory.connect(signers.bob).deployWrapper( + await wrapperImplementation.getAddress(), + testTokenAddress, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ) + ).to.be.revertedWithCustomError(wrapperFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.bob.address); + }); + + describe("Parameter Validation", function () { + it("should revert when implementation_ is zero address", async function () { + const originalToken = ethers.ZeroAddress; + + await expect( + wrapperFactory.deployWrapper( + ethers.ZeroAddress, // implementation = zero address + originalToken, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ) + ).to.be.revertedWithCustomError(wrapperFactory, "ZeroAddressImplementation"); + }); + + it("should revert when implementation_ is not a contract (EOA)", async function () { + const originalToken = ethers.ZeroAddress; + const eoaAddress = signers.alice.address; // EOA, not a contract + + await expect( + wrapperFactory.deployWrapper( + eoaAddress, // EOA address instead of contract + originalToken, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ) + ).to.be.revertedWithCustomError(wrapperFactory, "ImplementationNotContract"); + }); + + it("should revert when admin_ is zero address", async function () { + const originalToken = ethers.ZeroAddress; + + await expect( + wrapperFactory.deployWrapper( + await wrapperImplementation.getAddress(), + originalToken, + await cUsdc.getAddress(), + await adminProvider.getAddress(), + ethers.ZeroAddress // admin = zero address + ) + ).to.be.revertedWithCustomError(wrapperFactory, "ZeroAddressAdmin"); + }); + + it("should allow originalToken_ to be zero address (for ETH wrapper)", async function () { + const tx = await wrapperFactory.deployWrapper( + await wrapperImplementation.getAddress(), + ethers.ZeroAddress, // ETH wrapper - valid + await cUsdc.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ); + + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + }); + }); + + describe("Event Emission", function () { + it("should emit WrapperDeployed event with correct parameters for ETH wrapper", async function () { + const originalToken = ethers.ZeroAddress; + const implementationAddress = await wrapperImplementation.getAddress(); + const cUsdcAddress = await cUsdc.getAddress(); + const coordinator = signers.deployer.address; + + const tx = await wrapperFactory.deployWrapper( + implementationAddress, + originalToken, + cUsdcAddress, + coordinator, + signers.deployer.address + ); + + const receipt = await tx.wait(); + const event = receipt?.logs.find( + (log: any) => { + try { + const parsed = wrapperFactory.interface.parseLog({ + topics: log.topics as string[], + data: log.data + }); + return parsed?.name === "WrapperDeployed"; + } catch { + return false; + } + } + ); + + expect(event).to.not.be.undefined; + + const parsedEvent = wrapperFactory.interface.parseLog({ + topics: event!.topics as string[], + data: event!.data + }); + + expect(parsedEvent!.args.wrapper).to.be.properAddress; + expect(parsedEvent!.args.originalToken).to.equal(originalToken); + expect(parsedEvent!.args.confidentialToken).to.equal(cUsdcAddress); + expect(parsedEvent!.args.implementation).to.equal(implementationAddress); + expect(parsedEvent!.args.deploymentCoordinator).to.equal(coordinator); + expect(parsedEvent!.args.admin).to.equal(signers.deployer.address); + }); + + it("should emit WrapperDeployed event with correct parameters for ERC20 wrapper", async function () { + const testToken = await deployTestERC20Fixture("Test Token", 6); + const testTokenAddress = await testToken.getAddress(); + const implementationAddress = await wrapperImplementation.getAddress(); + const cUsdcAddress = await cUsdc.getAddress(); + const coordinator = signers.deployer.address; + + const tx = await wrapperFactory.deployWrapper( + implementationAddress, + testTokenAddress, + cUsdcAddress, + coordinator, + signers.alice.address // Use alice as admin for variety + ); + + const receipt = await tx.wait(); + const event = receipt?.logs.find( + (log: any) => { + try { + const parsed = wrapperFactory.interface.parseLog({ + topics: log.topics as string[], + data: log.data + }); + return parsed?.name === "WrapperDeployed"; + } catch { + return false; + } + } + ); + + expect(event).to.not.be.undefined; + + const parsedEvent = wrapperFactory.interface.parseLog({ + topics: event!.topics as string[], + data: event!.data + }); + + expect(parsedEvent!.args.wrapper).to.be.properAddress; + expect(parsedEvent!.args.originalToken).to.equal(testTokenAddress); + expect(parsedEvent!.args.confidentialToken).to.equal(cUsdcAddress); + expect(parsedEvent!.args.implementation).to.equal(implementationAddress); + expect(parsedEvent!.args.deploymentCoordinator).to.equal(coordinator); + expect(parsedEvent!.args.admin).to.equal(signers.alice.address); + }); + }); + }); + + describe("Ownership Management", function () { + it("should support two-step ownership transfer", async function () { + await wrapperFactory.transferOwnership(signers.alice.address); + + expect(await wrapperFactory.pendingOwner()).to.equal(signers.alice.address); + + expect(await wrapperFactory.owner()).to.equal(signers.deployer.address); + + await wrapperFactory.connect(signers.alice).acceptOwnership(); + + expect(await wrapperFactory.owner()).to.equal(signers.alice.address); + expect(await wrapperFactory.pendingOwner()).to.equal(ethers.ZeroAddress); + }); + + it("should prevent non-pending owner from accepting ownership", async function () { + await wrapperFactory.transferOwnership(signers.alice.address); + + await expect( + wrapperFactory.connect(signers.bob).acceptOwnership() + ).to.be.revertedWithCustomError(wrapperFactory, "OwnableUnauthorizedAccount") + .withArgs(signers.bob.address); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/WrapperUpgradeable.initialize.ts b/protocol-contracts/confidential-wrapper/test/WrapperUpgradeable.initialize.ts new file mode 100644 index 0000000000..60db7ec6f0 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/WrapperUpgradeable.initialize.ts @@ -0,0 +1,177 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { getSigners, Signers } from "./signers"; +import { WrapperUpgradeable, AdminProvider, RegulatedERC7984Upgradeable } from "../types"; +import { deployAdminProviderFixture, deployConfidentialErc20Fixture } from "./fixtures"; +import { ERC1967Proxy__factory } from "../types"; + +describe("WrapperUpgradeable - Initialize Validation", function () { + let signers: Signers; + let wrapperImplementation: WrapperUpgradeable; + let adminProvider: AdminProvider; + let cToken: RegulatedERC7984Upgradeable; + + before(async function () { + signers = await getSigners(); + }); + + beforeEach(async function () { + // Deploy WrapperUpgradeable implementation + const wrapperUpgradeableFactory = await ethers.getContractFactory("WrapperUpgradeable"); + wrapperImplementation = await wrapperUpgradeableFactory.deploy() as WrapperUpgradeable; + await wrapperImplementation.waitForDeployment(); + + // Deploy admin provider + ({ adminProvider } = await deployAdminProviderFixture(signers)); + + // Deploy confidential token + ({ cErc20: cToken } = await deployConfidentialErc20Fixture(signers, adminProvider)); + }); + + describe("Zero Address Validation", function () { + it("should revert when confidentialToken_ is zero address", async function () { + const originalToken = ethers.ZeroAddress; // ETH wrapper + const implAddress = await wrapperImplementation.getAddress(); + + // Create initialize call data with zero address for confidentialToken + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + originalToken, + ethers.ZeroAddress, // confidentialToken_ = zero address + await adminProvider.getAddress(), + signers.deployer.address + ]); + + // Deploy proxy with initialization + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + + await expect( + proxyFactory.deploy(implAddress, initData) + ).to.be.revertedWithCustomError(wrapperImplementation, "ZeroAddressConfidentialToken"); + }); + + it("should revert when deploymentCoordinator_ is zero address", async function () { + const originalToken = ethers.ZeroAddress; // ETH wrapper + const implAddress = await wrapperImplementation.getAddress(); + + // Create initialize call data with zero address for adminProvider + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + originalToken, + await cToken.getAddress(), + ethers.ZeroAddress, // deploymentCoordinator_ = zero address + signers.deployer.address + ]); + + // Deploy proxy with initialization + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + + await expect( + proxyFactory.deploy(implAddress, initData) + ).to.be.revertedWithCustomError(wrapperImplementation, "ZeroAddressDeploymentCoordinator"); + }); + + it("should revert when both confidentialToken_ and adminProvider_ are zero addresses", async function () { + const originalToken = ethers.ZeroAddress; // ETH wrapper + const implAddress = await wrapperImplementation.getAddress(); + + // Create initialize call data with zero addresses for both + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + originalToken, + ethers.ZeroAddress, // confidentialToken_ = zero address + ethers.ZeroAddress, // adminProvider_ = zero address + signers.deployer.address + ]); + + // Deploy proxy with initialization + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + + // Should revert with the first check (confidentialToken) + await expect( + proxyFactory.deploy(implAddress, initData) + ).to.be.revertedWithCustomError(wrapperImplementation, "ZeroAddressConfidentialToken"); + }); + + it("should allow originalToken_ to be zero address (ETH wrapper)", async function () { + const implAddress = await wrapperImplementation.getAddress(); + + const coordinator = signers.deployer.address; + + // Create initialize call data with zero address for originalToken (valid for ETH) + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + ethers.ZeroAddress, // originalToken_ = zero address (ETH) + await cToken.getAddress(), + coordinator, + signers.deployer.address + ]); + + // Deploy proxy with initialization + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + const proxy = await proxyFactory.deploy(implAddress, initData); + await proxy.waitForDeployment(); + + // Attach wrapper interface to proxy + const wrapper = await ethers.getContractAt("WrapperUpgradeable", await proxy.getAddress()); + + // Verify initialization succeeded + expect(await wrapper.originalToken()).to.equal(ethers.ZeroAddress); + expect(await wrapper.confidentialToken()).to.equal(await cToken.getAddress()); + expect(await wrapper.deploymentCoordinator()).to.equal(coordinator); + }); + }); + + describe("Prevent Re-initialization", function () { + it("should prevent calling initialize twice on the same proxy", async function () { + const implAddress = await wrapperImplementation.getAddress(); + + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + ethers.ZeroAddress, + await cToken.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ]); + + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + const proxy = await proxyFactory.deploy(implAddress, initData); + await proxy.waitForDeployment(); + + const wrapper = await ethers.getContractAt("WrapperUpgradeable", await proxy.getAddress()); + + // Try to initialize again + await expect( + wrapper.initialize( + ethers.ZeroAddress, + await cToken.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ) + ).to.be.revertedWithCustomError(wrapper, "InvalidInitialization"); + }); + }); + + describe("Receiver Entry Getter", function () { + it("should return empty ReceiverEntry for non-existent requestId", async function () { + const implAddress = await wrapperImplementation.getAddress(); + + const initData = wrapperImplementation.interface.encodeFunctionData("initialize", [ + ethers.ZeroAddress, + await cToken.getAddress(), + await adminProvider.getAddress(), + signers.deployer.address + ]); + + const proxyFactory = await ethers.getContractFactory("ERC1967Proxy"); + const proxy = await proxyFactory.deploy(implAddress, initData); + await proxy.waitForDeployment(); + + const wrapper = await ethers.getContractAt("WrapperUpgradeable", await proxy.getAddress()); + + // Query non-existent requestId + const receiverEntry = await wrapper.getReceiverEntry(999); + + // Verify all fields are zero/empty + expect(receiverEntry.to).to.equal(ethers.ZeroAddress); + expect(receiverEntry.refund).to.equal(ethers.ZeroAddress); + expect(receiverEntry.callbackData).to.equal("0x"); + expect(receiverEntry.committedFeeBasisPoints).to.equal(0); + }); + }); +}); diff --git a/protocol-contracts/confidential-wrapper/test/constants.ts b/protocol-contracts/confidential-wrapper/test/constants.ts new file mode 100644 index 0000000000..4dc0cbaa61 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/constants.ts @@ -0,0 +1 @@ +export const MAX_DECIMALS = 6; diff --git a/protocol-contracts/confidential-wrapper/test/fixtures.ts b/protocol-contracts/confidential-wrapper/test/fixtures.ts new file mode 100644 index 0000000000..97009a5de2 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/fixtures.ts @@ -0,0 +1,249 @@ +import { ethers, upgrades } from "hardhat"; +import { AdminProvider, AdminProvider__factory, SanctionsList, SanctionsList__factory, FeeManager, FeeManager__factory, TestERC20__factory, TestERC20, DeploymentCoordinator, ConfidentialTokenFactory__factory, ConfidentialTokenFactory, WrapperFactory__factory, DeploymentCoordinator__factory, WrapperFactory, FeeManagerInteractor__factory, FeeManagerInteractor, RegulatedERC7984Upgradeable__factory, UniswapV2Factory__factory, UniswapV2Factory, SwapV0__factory, SwapV0, ERC7984TransferBatcher__factory, ERC7984TransferBatcher } from "../types"; +import { Signers } from "./signers"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import UniswapV2FactoryArtifact from '@uniswap/v2-core/build/UniswapV2Factory.json'; + + +export async function deploySanctionsListFixture() { + const factory = (await ethers.getContractFactory("SanctionsList")) as SanctionsList__factory; + const sanctionsList = (await factory.deploy()) as SanctionsList; + const sanctionsListAddress = await sanctionsList.getAddress(); + return { sanctionsList, sanctionsListAddress }; +} + +export async function deployFeeManagerFixture(royalties: HardhatEthersSigner) { + const [deployer] = await ethers.getSigners(); + const factory = (await ethers.getContractFactory("FeeManager")) as FeeManager__factory; + const feeManager = (await factory.deploy( + 100, // 1% wrap fee basis points + 100, // 1% unwrap fee basis points + ethers.parseEther("0.01"), // deploy fee + ethers.parseEther("0.001"), // batch transfer fee + royalties, + )) as FeeManager; + const feeManagerAddress = await feeManager.getAddress(); + + // Grant FEE_MANAGER_ROLE to the deployer for test purposes + const FEE_MANAGER_ROLE = await feeManager.FEE_MANAGER_ROLE(); + await feeManager.grantRole(FEE_MANAGER_ROLE, deployer.address); + + return { feeManager, feeManagerAddress }; +} + +export async function deployTestERC20Fixture(symbol: string, decimals: number = 6, name?: string) { + const factory = (await ethers.getContractFactory("TestERC20")) as TestERC20__factory; + if (name == undefined) { + name = symbol; + } + return (await factory.deploy(name, symbol, decimals)) as TestERC20; +} + +export async function deployTestUnsafeERC20Fixture(contract_name: string, symbol: string, decimals: number = 6, name?: string) { + const factory = (await ethers.getContractFactory(contract_name)) as TestERC20__factory; + if (name == undefined) { + name = symbol; + } + return (await factory.deploy(name, symbol, decimals)) as TestERC20; +} + +export async function deployERC20NoDecimalsFixture() { + const ERC20NoDecimalsContract = await ethers.getContractFactory("ERC20NoDecimals"); + const tokenNoDecimals = await ERC20NoDecimalsContract.deploy("NoDecimalsToken", "NDT"); + await tokenNoDecimals.waitForDeployment(); + const tokenAsTestERC20 = await ethers.getContractAt("TestERC20", await tokenNoDecimals.getAddress()); + return { tokenNoDecimals: tokenAsTestERC20 }; +} + +export async function deployERC20InvalidDecimalsFixture() { + const ERC20InvalidDecimalsContract = await ethers.getContractFactory("ERC20InvalidDecimals"); + const tokenInvalidDecimals = await ERC20InvalidDecimalsContract.deploy("InvalidDecimalsToken", "IDT"); + await tokenInvalidDecimals.waitForDeployment(); + const tokenAsTestERC20 = await ethers.getContractAt("TestERC20", await tokenInvalidDecimals.getAddress()); + return { tokenInvalidDecimals: tokenAsTestERC20 }; +} + +export async function deployAdminProviderFixture(signers: Signers, sanctionsList?: SanctionsList) { + // Deploy and configure FeeManager with default fees + const { feeManager, feeManagerAddress } = await deployFeeManagerFixture(signers.royalties); + + if (sanctionsList === undefined) { + ({ sanctionsList } = await deploySanctionsListFixture()); + } + const sanctionsListAddress = await sanctionsList.getAddress(); + + const adminProviderFactory = (await ethers.getContractFactory("AdminProvider")) as AdminProvider__factory; + const adminProvider = (await adminProviderFactory.deploy(feeManagerAddress, sanctionsListAddress, signers.regulator)) as AdminProvider; + const adminProviderAddress = await adminProvider.getAddress(); + if (sanctionsList) { + await adminProvider.setSanctionsList(sanctionsList); + } + + return { adminProvider, adminProviderAddress, feeManager, feeManagerAddress, sanctionsList, sanctionsListAddress }; +} + +export async function deployConfidentialErc20Fixture(signers: Signers, adminProvider?: AdminProvider, underlying?: TestERC20, deployerIsWrapper = true) { + // Deploy AdminProvider if not provided + if (!adminProvider) { + ({ adminProvider } = await deployAdminProviderFixture(signers)); + } + const adminProviderAddress = await adminProvider.getAddress(); + const { coordinator } = await deployCoordinator(signers, adminProvider); + + const ERC7984 = await ethers.getContractFactory("RegulatedERC7984Upgradeable"); + const underlyingAddress = underlying ? await underlying.getAddress() : ethers.ZeroAddress; + const cErc20 = await upgrades.deployProxy(ERC7984, [ + "Naraggara", + "NARA", + 6, + signers.deployer.address, // admin + 1, // rate + underlyingAddress, + await coordinator.getAddress(), + signers.deployer.address // wrapperSetter (deployer will call setWrapper if needed) + ]); + await cErc20.waitForDeployment(); + if (deployerIsWrapper) { + await cErc20.setWrapper(signers.deployer.address); + } + const cErc20Address = await cErc20.getAddress(); + + return { cErc20, cErc20Address, adminProvider, adminProviderAddress }; +} + +export async function deployBurnableConfidentialErc20Fixture(signers: Signers, adminProvider?: AdminProvider, deployerIsWrapper = true) { + // Deploy AdminProvider if not provided + if (!adminProvider) { + ({ adminProvider } = await deployAdminProviderFixture(signers)); + } + const adminProviderAddress = await adminProvider.getAddress(); + const { coordinator } = await deployCoordinator(signers, adminProvider); + + + const burnableErc7984Factory = await ethers.getContractFactory("BurnableRegulatedERC7984Upgradeable"); + const burnableCErc20 = await upgrades.deployProxy(burnableErc7984Factory, [ + "Naraggara", + "NARA", + 6, + signers.deployer.address, // admin + 1, // rate + ethers.ZeroAddress, + await coordinator.getAddress(), + signers.deployer.address // wrapperSetter + ]); + await burnableCErc20.waitForDeployment(); + if (deployerIsWrapper) { + await burnableCErc20.setWrapper(signers.deployer.address); + } + const burnableCErc20Address = await burnableCErc20.getAddress(); + + return { burnableCErc20, burnableCErc20Address, adminProvider, adminProviderAddress }; +} + + +export async function deployWrapperFactoryFixture(owner?: HardhatEthersSigner) { + const wrapperFactory = (await ethers.getContractFactory("WrapperFactory")) as WrapperFactory__factory; + const deployedWrapperFactory = (await wrapperFactory.deploy()) as WrapperFactory; + const wrapperFactoryAddress = await deployedWrapperFactory.getAddress(); + + if (owner) { + await deployedWrapperFactory.transferOwnership(await owner.getAddress()); + } + + return { wrapperFactory: deployedWrapperFactory, wrapperFactoryAddress }; +} + +export async function deployConfidentialTokenFactoryFixture(owner?: HardhatEthersSigner) { + const confidentialTokenFactory = (await ethers.getContractFactory("RegulatedERC7984UpgradeableFactory")) as ConfidentialTokenFactory__factory; + const deployedConfidentialTokenFactory = (await confidentialTokenFactory.deploy()) as ConfidentialTokenFactory; + const confidentialTokenFactoryAddress = await deployedConfidentialTokenFactory.getAddress(); + + if (owner) { + await deployedConfidentialTokenFactory.transferOwnership(await owner.getAddress()); + } + + return { confidentialTokenFactory: deployedConfidentialTokenFactory, confidentialTokenFactoryAddress }; +} + +export async function deployCoordinator(signers: Signers, adminProvider?: AdminProvider) { + if (!adminProvider) { + ({ adminProvider } = await deployAdminProviderFixture(signers)); + } + + const { confidentialTokenFactory } = await deployConfidentialTokenFactoryFixture(); + const { wrapperFactory } = await deployWrapperFactoryFixture(); + + const coordinatorFactory = (await ethers.getContractFactory("DeploymentCoordinator")) as DeploymentCoordinator__factory; + const coordinator = (await coordinatorFactory.deploy( + adminProvider, + wrapperFactory, + confidentialTokenFactory + )) as DeploymentCoordinator; + + // Set coordinator as owner of both factories (using 2-step ownership transfer) + await confidentialTokenFactory.transferOwnership(await coordinator.getAddress()); + await wrapperFactory.transferOwnership(await coordinator.getAddress()); + + // Accept ownership from the coordinator side + await coordinator.acceptConfidentialTokenFactoryOwnership(); + await coordinator.acceptWrapperFactoryOwnership(); + + return { adminProvider, coordinator, confidentialTokenFactory, wrapperFactory }; +} + +export async function deployWrapperFixture(signers: Signers) { + const { adminProvider, coordinator, confidentialTokenFactory, wrapperFactory } = await deployCoordinator(signers); + + // Deploy wrapper implementation contract + const wrapperUpgradeableFactory = await ethers.getContractFactory("WrapperUpgradeable"); + const wrapperImplementation = await wrapperUpgradeableFactory.deploy(); + await wrapperImplementation.waitForDeployment(); + + // Set wrapper implementation on coordinator + await coordinator.setWrapperImplementation(await wrapperImplementation.getAddress()); + + // For compatibility with existing tests, we can return a "mock" wrapper structure + // Tests will need to use coordinator.deploy() to actually create wrapper/token pairs + const coordinatorAddress = await coordinator.getAddress(); + + return { + coordinator, + coordinatorAddress, + adminProvider, + confidentialTokenFactory, + wrapperFactory, + wrapperImplementation, + }; +} + +export async function deployUniswapFactoryFixture(deployer: HardhatEthersSigner) { + const WETH9 = await ethers.getContractFactory("WETH9"); + const weth = await WETH9.deploy(); + + const Factory = (await ethers.getContractFactory( + UniswapV2FactoryArtifact.abi, + UniswapV2FactoryArtifact.bytecode, + deployer, + )) as UniswapV2Factory__factory; + const factory = (await Factory.deploy(deployer.address)) as UniswapV2Factory; + + const Router = await ethers.getContractFactory("UniswapV2Router02"); + const router = await Router.deploy(factory.target, weth.target); + + return { router, factory, weth }; +} + + +export async function deploySwapV0Fixture(coordinator: DeploymentCoordinator) { + const factory = (await ethers.getContractFactory("SwapV0")) as SwapV0__factory; + const swapV0 = (await factory.deploy(coordinator)) as SwapV0; + return { swapV0, swapV0Address: await swapV0.getAddress() }; +} + +export async function deployTransferBatcherFixture(deployer: HardhatEthersSigner, adminProvider: AdminProvider) { + const transferBatcherFactory = (await ethers.getContractFactory("ERC7984TransferBatcher")) as ERC7984TransferBatcher__factory; + const transferBatcher = (await transferBatcherFactory.connect(deployer).deploy(adminProvider)) as ERC7984TransferBatcher; + const transferBatcherAddress = await transferBatcher.getAddress(); + return { transferBatcher, transferBatcherAddress }; +} diff --git a/protocol-contracts/confidential-wrapper/test/signers.ts b/protocol-contracts/confidential-wrapper/test/signers.ts new file mode 100644 index 0000000000..39fa1159f8 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/signers.ts @@ -0,0 +1,22 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers } from "hardhat"; + +const ACCOUNT_NAMES = ["deployer", "admin", "regulator", "royalties", "alice", "bob", "charlie", "delta"] as const; + +type AccountNames = (typeof ACCOUNT_NAMES)[number]; + +export type Signers = { + [K in AccountNames]: HardhatEthersSigner; +}; + +export const getSigners = async (): Promise => { + const signers: Signers = {} as Signers; + + const ethSigners = await ethers.getSigners(); + for (let index = 0; index < ACCOUNT_NAMES.length; index++) { + const name = ACCOUNT_NAMES[index] as AccountNames; + signers[name] = ethSigners[index]; + } + + return signers; +}; diff --git a/protocol-contracts/confidential-wrapper/test/utils.ts b/protocol-contracts/confidential-wrapper/test/utils.ts new file mode 100644 index 0000000000..64b23a4636 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/test/utils.ts @@ -0,0 +1,492 @@ +import { TransactionReceipt } from "ethers"; +import { ethers, fhevm } from "hardhat"; +import { BurnableRegulatedERC7984Upgradeable, DeploymentCoordinator, FeeManager, RegulatedERC7984Upgradeable, TestERC20, WrapperUpgradeable } from "../types"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { FhevmType } from "@fhevm/hardhat-plugin"; +import { expect } from "chai"; +import { ClearValueType } from "@zama-fhe/relayer-sdk"; + +export const getEventFromABI = (receipt: TransactionReceipt | null, ABI: string) => { + if (receipt === null) { + throw "receipt is null"; + } + const iface = new ethers.Interface([ABI]); + + const events = []; + + for (const log of receipt.logs) { + try { + const parsed = iface.parseLog(log); + if (parsed) { + events.push(parsed); + } + } catch { + // not your event, ignore + } + } + + return events; +}; + +export const getDebugEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event Debug(string varType, string varName, bytes value)"); + +export const debug = async (receipt: TransactionReceipt | null) => { + const abiCoder = new ethers.AbiCoder(); + const events = getDebugEvent(receipt); + + for (const event of events) { + const varType = event.args[0]; + const decrypt = async (value: string) => { + if (varType == "euint64") { + return await fhevm.publicDecryptEuint(FhevmType.euint64, abiCoder.decode(["bytes32"], value)[0]); + } else if (varType == "address") { + return abiCoder.decode(["address"], value)[0]; + } + } + const varName = event.args[1]; + const value = await decrypt(event.args[2]); + console.log(`[debug] ${varType} ${varName} = ${value}`); + } +} + +export const getTransferInfoEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event TransferInfo(address indexed from, address indexed to, bytes32 encryptedAmount, uint256 txId)"); + +export const getTransferFeeInfoEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event TransferFeeInfo(address indexed from, address indexed to, uint256 txId, uint64 feeAmount)"); + +export const getBurnEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event BurnInfo(address indexed from, bytes32 amount, uint256 txId)"); + +export const getMintEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event MintInfo(address indexed to, uint64 amount, uint256 txId)"); + +export const getTokenRegulatorUpdatedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event TokenRegulatorUpdated(address indexed oldRegulator, address indexed newRegulator)"); + +export const getWrappedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event Wrapped(uint64 mintAmount, uint256 amountIn, uint256 feeAmount, address indexed to_, uint256 indexed mintTxId)"); + +export const getWrapDeployedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI( + receipt, + "event WrapperPairDeployed(address indexed originalToken, address indexed wrapper, address indexed confidentialToken, string originalName, string originalSymbol, uint8 originalDecimals, address deployer)", + ); + +export const getBatchTransferEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event BatchTransfer(address indexed cToken, address indexed sender, uint256 startTxId, uint256 endTxId, uint256 fee)"); + +export const getRetryTransferEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event RetryTransfer(address indexed cToken, address indexed sender, uint256 originalTxId, uint256 retryTxId)"); + +export const getUnwrapStartedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event UnwrappedStarted(bool returnVal, uint256 indexed requestId, uint256 indexed txId, address indexed to, address refund, bytes32 requestedAmount, bytes32 burnAmount)"); + +export const getUnwrapFinalizedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event UnwrappedFinalized(uint256 indexed requestId, bool finalizeSuccess, bool feeTransferSuccess, uint64 burnAmount, uint256 unwrapAmount, uint256 feeAmount, uint256 indexed nextTxId)"); + +export const getSwapStartedEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event SwapStarted(bool success, string errorString, bytes errorLowLevelData)"); + +export const getSwapEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event Swap(bool indexed success, address[] path, uint256 indexed unwrapTxId, uint256 wrapTxId, string errorReasonString, bytes errorLowLevelData)"); + +export const getERC20TransferEvent = (receipt: TransactionReceipt | null) => + getEventFromABI(receipt, "event Transfer(address indexed from, address indexed to, uint256 value)"); + +export async function getTxHashes(blockNumber: number) { + const block = await ethers.provider.getBlock(blockNumber); + if (block === null) { + throw new Error("block is null"); + } + return block.transactions; +} + +export async function getConfidentialBalance( + confidentialToken: RegulatedERC7984Upgradeable | BurnableRegulatedERC7984Upgradeable, + user: HardhatEthersSigner, + signer?: HardhatEthersSigner, +) { + if (!signer) { + signer = user; + } + const balanceHandleBob = await confidentialToken.confidentialBalanceOf(user); + const confidentialTokenAddress = await confidentialToken.getAddress(); + return await fhevm.userDecryptEuint( + FhevmType.euint64, + balanceHandleBob, + confidentialTokenAddress, + signer, + ); +} + +export async function getWrapFeeBasisPoints(wrapper: WrapperUpgradeable) { + const adminProviderAddress = await wrapper.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + return feeManager.wrapFeeBasisPoints(); +} + +export async function getUnwrapFeeBasisPoints(wrapper: WrapperUpgradeable) { + const adminProviderAddress = await wrapper.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + return feeManager.unwrapFeeBasisPoints(); +} + +export async function getUnwrapFee(wrapper: WrapperUpgradeable, amount: bigint) { + const unwrapFeeBasisPoints = await getUnwrapFeeBasisPoints(wrapper); + return (amount * unwrapFeeBasisPoints + 10_000n - 1n) / BigInt(10_000); +} + +export const getDeployConfidentialTokenTx = async (coordinator: DeploymentCoordinator, erc20: TestERC20, signer: HardhatEthersSigner) => { + // Get deploy fee from the coordinator's admin provider + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + return coordinator.connect(signer).deploy(await erc20.getAddress(), { + value: deployTokenFee, + }); +}; + +export const deployConfidentialToken = async (coordinator: DeploymentCoordinator, erc20: TestERC20, signer: HardhatEthersSigner) => { + // Get deploy fee from the coordinator's admin provider + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + const deployToken = await coordinator.connect(signer).deploy(erc20, { value: deployTokenFee }); + const receipt = await deployToken.wait(); + + const cTokenAddress = await coordinator.getConfidentialToken(erc20); + const wrapperAddress = await coordinator.getWrapper(erc20); + const wrapper = await ethers.getContractAt("WrapperUpgradeable", wrapperAddress); + + return { + cTokenAddress, + cToken: await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress), + wrapperAddress, + wrapper, + receipt, + }; +}; + +export const deployConfidentialETH = async (coordinator: DeploymentCoordinator, signer: HardhatEthersSigner) => { + // Get deploy fee from the coordinator's admin provider + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const deployTokenFee = await feeManager.getDeployFee(ethers.ZeroAddress); + + const deployToken = await coordinator.connect(signer).deploy(ethers.ZeroAddress, { value: deployTokenFee }); + const receipt = await deployToken.wait(); + + const cEthAddress = await coordinator.getConfidentialToken(ethers.ZeroAddress); + const wrapperAddress = await coordinator.getWrapper(ethers.ZeroAddress); + const wrapper = await ethers.getContractAt("WrapperUpgradeable", wrapperAddress); + + return { + cEthAddress, + cEth: await ethers.getContractAt("RegulatedERC7984Upgradeable", cEthAddress), + wrapperAddress, + wrapper, + receipt, + }; +}; + +export const wrapETH = async (coordinator: DeploymentCoordinator, amount: bigint, recipient: string, signer: HardhatEthersSigner) => { + // Get the deployed wrapper for ETH + const wrapperAddress = await coordinator.getWrapper(ethers.ZeroAddress); + const wrapper = await ethers.getContractAt("WrapperUpgradeable", wrapperAddress); + + // Get the confidential token to access its rate + const cTokenAddress = await coordinator.getConfidentialToken(ethers.ZeroAddress); + const cToken = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress); + const rate = await cToken.rate(); + + // Calculate scaled amount and fee based on scaled amount + const scaledAmount = amount / rate; + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFee = await feeManager.getWrapFee(scaledAmount, signer.address, recipient); + + const wrap = await wrapper.connect(signer).wrap(recipient, amount, { value: amount }); + const wrapReceipt = await wrap.wait(); + + return { wrapReceipt, wrapFee }; +}; + +export const wrapERC20 = async ( + coordinator: DeploymentCoordinator, + erc20: TestERC20, + amount: bigint, + recipient: string, + signer: HardhatEthersSigner, +) => { + // Get the deployed wrapper for this ERC20 + const wrapperAddress = await coordinator.getWrapper(await erc20.getAddress()); + const wrapper = await ethers.getContractAt("WrapperUpgradeable", wrapperAddress); + + // Get the confidential token to access its rate + const cTokenAddress = await coordinator.getConfidentialToken(await erc20.getAddress()); + const cToken = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress); + const rate = await cToken.rate(); + + // Calculate scaled amount and fee based on scaled amount + const scaledAmount = amount / rate; + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + const wrapFee = await feeManager.getWrapFee(scaledAmount, signer.address, recipient); + + // Calculate actual transfer amount (rounded down to nearest multiple of rate) + const actualTransferAmount = (amount / rate) * rate; + + const approve = await erc20.connect(signer).approve(wrapperAddress, actualTransferAmount); + await approve.wait(); + + const wrap = await wrapper.connect(signer).wrap(recipient, amount); + const wrapReceipt = await wrap.wait(); + + return { wrapReceipt, wrapFee }; +}; + +export const confidentialApprove = async ( + cToken: RegulatedERC7984Upgradeable, + spender: string, + amount: bigint, + signer: HardhatEthersSigner, +) => { + const encryptedAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signer.address) + .add64(amount) + .encrypt(); + + const confidentialApprove = await cToken + .connect(signer) + ["confidentialApprove(address,bytes32,bytes)"](spender, encryptedAmount.handles[0], encryptedAmount.inputProof); + await confidentialApprove.wait(); +}; + +export const unwrapToken = async ( + wrapper: WrapperUpgradeable, + recipient: string, + amount: bigint, + signer: HardhatEthersSigner, + refund?: string, + callbackData?: string, +) => { + const cTokenAddress = await wrapper.confidentialToken(); + const cToken = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress); + + const unwrapFee = await getUnwrapFee(wrapper, amount); + + const encryptedUnwrapAmount = await fhevm + .createEncryptedInput(await cToken.getAddress(), signer.address) + .add64(amount) + .encrypt(); + + const abiCoder = new ethers.AbiCoder(); + const data = abiCoder.encode( + ["address", "address", "bytes"], + [recipient, refund || recipient, callbackData || "0x"], + ) + + const unwrapTx = await cToken.connect(signer)["confidentialTransferAndCall(address,bytes32,bytes,bytes)"]( + await wrapper.getAddress(), + encryptedUnwrapAmount.handles[0], + encryptedUnwrapAmount.inputProof, + data, + ); + const unwrapReceipt = await unwrapTx.wait(); + + const unwrapStartedEvents = getUnwrapStartedEvent(unwrapReceipt); + const unwrapStartedEvent = unwrapStartedEvents[0]; + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + const abiEncodedClearBurnResults = publicDecryptResults.abiEncodedClearValues; + const decryptionProof = publicDecryptResults.decryptionProof; + + const unwrapFinalizedTx = await wrapper.connect(signer).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + abiEncodedClearBurnResults, + decryptionProof, + ); + const unwrapFinalizedReceipt = await unwrapFinalizedTx.wait(); + + return { unwrapReceipt, unwrapFinalizedReceipt, unwrapFee }; +}; + +export const unwrapTokenOld = async ( + coordinator: DeploymentCoordinator, + tokenAddress: string, + recipient: string, + amount: bigint, + signer: HardhatEthersSigner, + data?: string, +) => { + if (data == undefined) { + data = "0x"; + } + // Get the deployed wrapper for this token + const wrapperAddress = await coordinator.getWrapper(tokenAddress); + const wrapper = await ethers.getContractAt("WrapperUpgradeable", wrapperAddress); + + const unwrapFee = await getUnwrapFee(wrapper, amount); + + const unwrapTx = await wrapper.connect(signer).unwrap(recipient, amount, data); + const unwrapReceipt = await unwrapTx.wait(); + + await fhevm.awaitDecryptionOracle(); + + const txHashes = await getTxHashes(await ethers.provider.getBlockNumber()); + expect(txHashes.length).to.be.equal(1); + const txHash = txHashes[0]; + const unwrapFinalizedReceipt = await ethers.provider.getTransactionReceipt(txHash); + + const cTokenAddress = await wrapper.confidentialToken(); + const cToken = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress); + + return { unwrapReceipt, unwrapFinalizedReceipt, unwrapFee }; +}; + +export const getEncryptedBalance = async (cToken: RegulatedERC7984Upgradeable, account: HardhatEthersSigner, tokenAddress: string) => { + const balanceHandle = await cToken.confidentialBalanceOf(account); + return await fhevm.userDecryptEuint(FhevmType.euint64, balanceHandle, tokenAddress, account); +}; + +export const getFeeManager = async (coordinator: DeploymentCoordinator) => { + const adminProviderAddress = await coordinator.adminProvider(); + const adminProvider = await ethers.getContractAt("AdminProvider", adminProviderAddress); + const feeManagerAddress = await adminProvider.feeManager(); + return await ethers.getContractAt("FeeManager", feeManagerAddress); +} + +export const getWrapFee = async (feeManager: FeeManager, amount: bigint, _addressFrom?: string, _addressTo?: string) => { + + return await feeManager.getWrapFee(amount, ethers.ZeroAddress, ethers.ZeroAddress) +} + +export const getBatchTransferFee = async (adminProvider: AdminProvider) => { + const feeManagerAddress = await adminProvider.feeManager(); + const feeManager = await ethers.getContractAt("FeeManager", feeManagerAddress); + return await feeManager.getBatchTransferFee(); +} + +/** + * Verifies the wrapper backing invariant: wrapper's underlying balance equals cToken total supply * rate + * This ensures all confidential tokens are properly backed by underlying tokens in the wrapper + * + * @param wrapper - Wrapper contract instance + * @param provider - Ethers provider (optional, defaults to ethers.provider) + */ +export const verifyWrapperBacking = async ( + wrapper: any, + provider?: any +) => { + const ethersProvider = provider || ethers.provider; + + // Get addresses from wrapper + const wrapperAddress = await wrapper.getAddress(); + const underlyingTokenAddress = await wrapper.originalToken(); + const cTokenAddress = await wrapper.confidentialToken(); + + // Get cToken contract + const cToken = await ethers.getContractAt("RegulatedERC7984Upgradeable", cTokenAddress); + + // Get wrapper's balance of underlying token + let wrapperBalance: bigint; + if (underlyingTokenAddress === ethers.ZeroAddress) { + // ETH balance + wrapperBalance = await ethersProvider.getBalance(wrapperAddress); + } else { + // ERC20 balance + const underlyingToken = await ethers.getContractAt("TestERC20", underlyingTokenAddress); + wrapperBalance = await underlyingToken.balanceOf(wrapperAddress); + } + + // Get confidential token total supply (encrypted) + const totalSupplyHandle = await cToken.confidentialTotalSupply(); + const totalSupply = await fhevm.publicDecryptEuint(FhevmType.euint64, totalSupplyHandle); + + // Ensure total supply is the same as the wrapper's counter + expect(await wrapper.mintedSupply()).to.equal(totalSupply); + + // Get rate + const rate = await cToken.rate(); + + // Calculate expected backing (totalSupply is in euint64 units, multiply by rate to get underlying units) + const expectedBacking = totalSupply * rate; + + // Verify invariant + if (wrapperBalance !== expectedBacking) { + throw new Error( + `Wrapper backing invariant violated!\n` + + ` Wrapper balance: ${wrapperBalance}\n` + + ` Expected (totalSupply * rate): ${expectedBacking}\n` + + ` Total supply: ${totalSupply}\n` + + ` Rate: ${rate}` + ); + } + + return { wrapperBalance, totalSupply, rate, expectedBacking }; +}; + +export const checkTotalSupply = async (erc7984: RegulatedERC7984Upgradeable, expectedAmount: number) => { + const totalSupplyHandle = await erc7984.confidentialTotalSupply(); + const totalSupply = await fhevm.publicDecryptEuint(FhevmType.euint64, totalSupplyHandle); + expect(totalSupply).to.equal(expectedAmount); +} + +/** + * Finalizes an unwrap operation using the public decrypt flow + * Replaces the old fhevm.awaitDecryptionOracle() pattern + * + * @param receipt - Transaction receipt from confidentialTransferAndCall + * @param wrapper - Wrapper contract instance + * @param signer - Signer to execute finalizeUnwrap (typically the unwrapper) + * @returns Receipt from the finalizeUnwrap transaction + */ +export const finalizeUnwrapFromReceipt = async ( + receipt: any, + wrapper: any, + signer: HardhatEthersSigner +) => { + // Extract UnwrapStarted event + const unwrapStartedEvents = getUnwrapStartedEvent(receipt); + expect(unwrapStartedEvents.length).to.be.equal(1); + const unwrapStartedEvent = unwrapStartedEvents[0]; + + // Publicly decrypt the handles + const publicDecryptResults = await fhevm.publicDecrypt([ + unwrapStartedEvent.args[5], + unwrapStartedEvent.args[6], + ]); + + const abiEncodedClearBurnResults = publicDecryptResults.abiEncodedClearValues; + const decryptionProof = publicDecryptResults.decryptionProof; + + // Finalize the unwrap with decrypted values + const finalizeUnwrapTx = await wrapper.connect(signer).finalizeUnwrap( + unwrapStartedEvent.args.requestId, + abiEncodedClearBurnResults, + decryptionProof, + ); + + return await finalizeUnwrapTx.wait(); +}; diff --git a/protocol-contracts/confidential-wrapper/tsconfig.json b/protocol-contracts/confidential-wrapper/tsconfig.json new file mode 100644 index 0000000000..1f7a7941c6 --- /dev/null +++ b/protocol-contracts/confidential-wrapper/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2022"], // get error cause (ErrorOptions) + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": true, + "removeComments": true, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "target": "es2022" // get error cause (ErrorOptions) + }, + "exclude": ["node_modules"], + "files": ["./hardhat.config.ts"], + "include": ["src/**/*", "tasks/**/*", "test/**/*", "deploy/**/*", "types/"] +}